From 55166b79ab99fc32a13a39080a1fe1760a01b2be Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Tue, 8 Oct 2024 11:41:54 +1100 Subject: [PATCH 1/8] python bindings: make pydoc happy with package metadata In order for help(pathrs) to correctly list all of the re-exported code from pathrs._pathrs we need to add the names to __all__ in the main module. Rather than duplicating the __all__ declaration we can just re-use the one defined in pathrs._pathrs. In addition, to avoid having __version__ drift away from the other version definitions, we can use importlib.metadata.version() to use the dist-info/ version (defined in pyproject.toml) so that pydoc just reflects the actual dist-info/ version information. Signed-off-by: Aleksa Sarai --- contrib/bindings/python/pathrs/__init__.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/contrib/bindings/python/pathrs/__init__.py b/contrib/bindings/python/pathrs/__init__.py index f11e495b..d201a8c8 100644 --- a/contrib/bindings/python/pathrs/__init__.py +++ b/contrib/bindings/python/pathrs/__init__.py @@ -15,7 +15,23 @@ # See the License for the specific language governing permissions and # limitations under the License. +import importlib +import importlib.metadata + +from . import _pathrs from ._pathrs import * -# TODO: Figure out a way to keep this version up-to-date with Cargo.toml. -__version__ = "0.0.2" +# In order get pydoc to include the documentation for the re-exported code from +# _pathrs, we need to include all of the members in __all__. Rather than +# duplicating the member list here explicitly, just re-export __all__. +__all__ = [] +__all__ += _pathrs.__all__ # pyright doesn't support "=" here. + +try: + # In order to avoid drift between this version and the dist-info/ version + # information, just fill __version__ with the dist-info/ information. + __version__ = importlib.metadata.version("pathrs") +except importlib.metadata.PackageNotFoundError: + # We're being run from a local directory without an installed version of + # pathrs, so just fill in a dummy version. + __version__ = "" From f7527ad3317c38c02af0fbe34571d1773c5b64c2 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Sun, 6 Oct 2024 11:55:12 +1100 Subject: [PATCH 2/8] python bindings: fix bugs noticed by mypy Most of these are because of function name changes we made but forgot to update all of the callers. There are also a few typing errors -- the only one we don't fix at the moment is Root.creat() because it turns out all our APIs (including Rust and Go) return a Handle when actually you want to return a regular file for this case. This will be fixed in a separate commit. Fixes: c7728ed94781 ("python bindings: switch to setuptools") Signed-off-by: Aleksa Sarai --- contrib/bindings/python/pathrs/_pathrs.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/contrib/bindings/python/pathrs/_pathrs.py b/contrib/bindings/python/pathrs/_pathrs.py index fedaef83..b3fb98c9 100644 --- a/contrib/bindings/python/pathrs/_pathrs.py +++ b/contrib/bindings/python/pathrs/_pathrs.py @@ -20,6 +20,7 @@ import re import sys import copy +import errno import fcntl from ._libpathrs_cffi import ffi, lib as libpathrs_so @@ -40,9 +41,6 @@ def _cstr(pystr): def _pystr(cstr): return ffi.string(cstr).decode("utf8") -def _pyptr(cptr): - return int(ffi.cast("uintptr_t", cptr)) - def _cbuffer(size): return ffi.new("char[%d]" % (size,)) @@ -118,8 +116,8 @@ def _fileno(file): # Assume there is a fileno method. return file.fileno() -def _clonefile(file): - return fcntl.fcntl(fileno(file), fcntl.F_DUPFD_CLOEXEC) +def _clonefile(file: FileLike) -> int: + return fcntl.fcntl(_fileno(file), fcntl.F_DUPFD_CLOEXEC) class WrappedFd(object): @@ -152,7 +150,7 @@ def __init__(self, file): # If this is a regular open file, we need to make a copy because # you cannot leak files and so the GC might close it from # underneath us. - fd = clonefd(fd) + fd = _clonefile(fd) self._fd = fd def fileno(self): @@ -503,7 +501,7 @@ def creat(self, path, filemode, mode="r", extra_flags=0): Atomically create-and-open a new file at the given path in the Root, a-la O_CREAT. - This method returns an os.fdopen() file handle. + This method returns a Handle. filemode is the Unix DAC mode you wish the new file to be created with. This mode might not be the actual mode of the created file due to a @@ -519,6 +517,7 @@ def creat(self, path, filemode, mode="r", extra_flags=0): fd = libpathrs_so.pathrs_creat(self.fileno(), path, flags, filemode) if fd < 0: raise Error._fetch(fd) or INTERNAL_ERROR + # TODO: This should actually return an os.fdopen. return Handle(fd) # TODO: creat_raw? From 59a151cb7f70da5384904af2a6d052f69398328c Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Mon, 7 Oct 2024 23:15:40 +1100 Subject: [PATCH 3/8] python bindings: properly document Root.mknod's dev argument Signed-off-by: Aleksa Sarai --- contrib/bindings/python/pathrs/_pathrs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contrib/bindings/python/pathrs/_pathrs.py b/contrib/bindings/python/pathrs/_pathrs.py index b3fb98c9..2146510b 100644 --- a/contrib/bindings/python/pathrs/_pathrs.py +++ b/contrib/bindings/python/pathrs/_pathrs.py @@ -620,6 +620,10 @@ def mknod(self, path, mode, dev=0): mode of the created file due to a variety of external factors (umask, setgid bits, POSIX ACLs). + dev is the the (major, minor) device number used for the new inode if + the mode contains S_IFCHR or S_IFBLK. You can construct the device + number from a (major, minor) using os.makedev(). + A pathrs.Error is raised if the path already exists. """ path = _cstr(path) From 77a3d34f47652a7ca54aac87921548681d15bb33 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Sun, 6 Oct 2024 12:07:33 +1100 Subject: [PATCH 4/8] Root::create_file: return File not Handle for O_CREAT Since we let the user specify the open flags a-la Handle::reopen, it makes far more sense to return a File from Root::create_file than a Handle. The bindings also need to be updated to match these semantics. Signed-off-by: Aleksa Sarai --- contrib/bindings/python/pathrs/_pathrs.py | 3 +-- go-pathrs/root_linux.go | 10 +++------- src/capi/ret.rs | 11 ++++++++++- src/root.rs | 8 ++++---- src/tests/capi/root.rs | 10 +++++----- src/tests/traits/root.rs | 12 ++++++------ 6 files changed, 29 insertions(+), 25 deletions(-) diff --git a/contrib/bindings/python/pathrs/_pathrs.py b/contrib/bindings/python/pathrs/_pathrs.py index 2146510b..add9955d 100644 --- a/contrib/bindings/python/pathrs/_pathrs.py +++ b/contrib/bindings/python/pathrs/_pathrs.py @@ -517,8 +517,7 @@ def creat(self, path, filemode, mode="r", extra_flags=0): fd = libpathrs_so.pathrs_creat(self.fileno(), path, flags, filemode) if fd < 0: raise Error._fetch(fd) or INTERNAL_ERROR - # TODO: This should actually return an os.fdopen. - return Handle(fd) + return os.fdopen(fd, mode) # TODO: creat_raw? diff --git a/go-pathrs/root_linux.go b/go-pathrs/root_linux.go index 72e8134c..876d2062 100644 --- a/go-pathrs/root_linux.go +++ b/go-pathrs/root_linux.go @@ -106,21 +106,17 @@ func (r *Root) ResolveNoFollow(path string) (*Handle, error) { // Create creates a file within the Root's directory tree at the given path, // and returns a handle to the file. The provided mode is used for the new file // (the process's umask applies). -func (r *Root) Create(path string, flags int, mode os.FileMode) (*Handle, error) { +func (r *Root) Create(path string, flags int, mode os.FileMode) (*os.File, error) { unixMode, err := toUnixMode(mode) if err != nil { return nil, err } - return withFileFd(r.inner, func(rootFd uintptr) (*Handle, error) { + return withFileFd(r.inner, func(rootFd uintptr) (*os.File, error) { handleFd, err := pathrsCreat(rootFd, path, flags, unixMode) if err != nil { return nil, err } - handleFile, err := mkFile(uintptr(handleFd)) - if err != nil { - return nil, err - } - return &Handle{inner: handleFile}, nil + return mkFile(uintptr(handleFd)) }) } diff --git a/src/capi/ret.rs b/src/capi/ret.rs index f2a988a4..e219cfc4 100644 --- a/src/capi/ret.rs +++ b/src/capi/ret.rs @@ -19,7 +19,10 @@ use crate::{capi::error as capi_error, error::Error, Handle, Root}; -use std::os::unix::io::{IntoRawFd, OwnedFd}; +use std::{ + fs::File, + os::unix::io::{IntoRawFd, OwnedFd}, +}; use libc::c_int; @@ -64,6 +67,12 @@ impl IntoCReturn for Handle { } } +impl IntoCReturn for File { + fn into_c_return(self) -> CReturn { + OwnedFd::from(self).into_c_return() + } +} + impl IntoCReturn for Result where V: IntoCReturn, diff --git a/src/root.rs b/src/root.rs index 192457b4..d78f6769 100644 --- a/src/root.rs +++ b/src/root.rs @@ -29,7 +29,7 @@ use crate::{ }; use std::{ - fs::Permissions, + fs::{File, Permissions}, io::Error as IOError, os::unix::{ ffi::OsStrExt, @@ -367,7 +367,7 @@ impl Root { path: P, flags: OpenFlags, perm: &Permissions, - ) -> Result { + ) -> Result { self.as_ref().create_file(path, flags, perm) } @@ -829,7 +829,7 @@ impl RootRef<'_> { path: P, mut flags: OpenFlags, perm: &Permissions, - ) -> Result { + ) -> Result { // The path doesn't exist yet, so we need to get a safe reference to the // parent and just operate on the final (slashless) component. let (dir, name) = self @@ -851,7 +851,7 @@ impl RootRef<'_> { } })?; - Ok(Handle::from_fd_unchecked(fd)) + Ok(fd.into()) } /// Within the [`RootRef`]'s tree, create a directory and any of its parent diff --git a/src/tests/capi/root.rs b/src/tests/capi/root.rs index 221ca040..6a4df1b0 100644 --- a/src/tests/capi/root.rs +++ b/src/tests/capi/root.rs @@ -32,7 +32,7 @@ use crate::{ }; use std::{ - fs::Permissions, + fs::{File, Permissions}, os::unix::{ fs::PermissionsExt, io::{AsFd, BorrowedFd, OwnedFd}, @@ -140,14 +140,14 @@ impl CapiRoot { path: P, flags: OpenFlags, perm: &Permissions, - ) -> Result { + ) -> Result { let root_fd = self.inner.as_fd(); let path = capi_utils::path_to_cstring(path); capi_utils::call_capi_fd(|| unsafe { capi::core::pathrs_creat(root_fd.into(), path.as_ptr(), flags.bits(), perm.mode()) }) - .map(CapiHandle::from_fd_unchecked) + .map(File::from) } fn mkdir_all>( @@ -269,7 +269,7 @@ impl RootImpl for CapiRoot { path: P, flags: OpenFlags, perm: &Permissions, - ) -> Result { + ) -> Result { self.create_file(path, flags, perm) } @@ -348,7 +348,7 @@ impl RootImpl for &CapiRoot { path: P, flags: OpenFlags, perm: &Permissions, - ) -> Result { + ) -> Result { CapiRoot::create_file(self, path, flags, perm) } diff --git a/src/tests/traits/root.rs b/src/tests/traits/root.rs index a186f1bb..16e14894 100644 --- a/src/tests/traits/root.rs +++ b/src/tests/traits/root.rs @@ -26,7 +26,7 @@ use crate::{ }; use std::{ - fs::Permissions, + fs::{File, Permissions}, os::unix::io::{AsFd, OwnedFd}, path::{Path, PathBuf}, }; @@ -57,7 +57,7 @@ pub(in crate::tests) trait RootImpl: AsFd + std::fmt::Debug + Sized { path: P, flags: OpenFlags, perm: &Permissions, - ) -> Result; + ) -> Result; fn mkdir_all>( &self, @@ -122,7 +122,7 @@ impl RootImpl for Root { path: P, flags: OpenFlags, perm: &Permissions, - ) -> Result { + ) -> Result { self.create_file(path, flags, perm) } @@ -199,7 +199,7 @@ impl RootImpl for &Root { path: P, flags: OpenFlags, perm: &Permissions, - ) -> Result { + ) -> Result { Root::create_file(self, path, flags, perm) } @@ -276,7 +276,7 @@ impl RootImpl for RootRef<'_> { path: P, flags: OpenFlags, perm: &Permissions, - ) -> Result { + ) -> Result { self.create_file(path, flags, perm) } @@ -353,7 +353,7 @@ impl RootImpl for &RootRef<'_> { path: P, flags: OpenFlags, perm: &Permissions, - ) -> Result { + ) -> Result { RootRef::create_file(self, path, flags, perm) } From ad816707c0572ac37f5145575eb8494cba3228b0 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Sun, 6 Oct 2024 12:10:35 +1100 Subject: [PATCH 5/8] python bindings: add type annotations This is just a first pass, trying to make the annotations make sense. We probably need to adjust some of them for packaging before the next release. The type annotations for libpathrs.so should be sufficient but it's a bit unfortunate that ffi.NULL can't be listed as a possible return value (a-la None with Optional). This makes the typing for errorinfo a little dodgy, and the Error._fetch typing also is not entirely idiomatic (maybe we need to have an overload to indicate that only actual ErrorIds return an Error). It would be nice if we could do Literal[0..] but that syntax doesn't exist. Signed-off-by: Aleksa Sarai --- CHANGELOG.md | 2 + contrib/bindings/python/Makefile | 2 +- contrib/bindings/python/pathrs/.gitignore | 2 +- .../pathrs/_libpathrs_cffi/__init__.pyi | 19 ++ .../python/pathrs/_libpathrs_cffi/lib.pyi | 83 +++++++ contrib/bindings/python/pathrs/_pathrs.py | 203 ++++++++++-------- .../bindings/python/pathrs/pathrs_build.py | 15 +- contrib/bindings/python/pathrs/py.typed | 0 contrib/bindings/python/pyproject.toml | 1 + 9 files changed, 228 insertions(+), 99 deletions(-) create mode 100644 contrib/bindings/python/pathrs/_libpathrs_cffi/__init__.pyi create mode 100644 contrib/bindings/python/pathrs/_libpathrs_cffi/lib.pyi create mode 100644 contrib/bindings/python/pathrs/py.typed diff --git a/CHANGELOG.md b/CHANGELOG.md index e5ddbc19..0f54a2bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixes ### - python bindings: add a minimal README for PyPI. - python bindings: actually export `PROC_ROOT`. +- python bindings: add type annotations and `py.typed` to allow for downstream + users to get proper type annotations for the API. ## [0.1.1] - 2024-10-01 ## diff --git a/contrib/bindings/python/Makefile b/contrib/bindings/python/Makefile index e1f72788..eb3364ad 100644 --- a/contrib/bindings/python/Makefile +++ b/contrib/bindings/python/Makefile @@ -19,7 +19,7 @@ PIP ?= pip3 SRC_FILES := $(wildcard *.py pathrs/*.py) -dist: $(SRC_FILES) +dist: $(SRC_FILES) pyproject.toml $(PYTHON) -m build .PHONY: clean diff --git a/contrib/bindings/python/pathrs/.gitignore b/contrib/bindings/python/pathrs/.gitignore index eea96bf6..c97e693e 100644 --- a/contrib/bindings/python/pathrs/.gitignore +++ b/contrib/bindings/python/pathrs/.gitignore @@ -1,2 +1,2 @@ /__pycache__/ -/_libpathrs_cffi* +/_libpathrs_cffi.* diff --git a/contrib/bindings/python/pathrs/_libpathrs_cffi/__init__.pyi b/contrib/bindings/python/pathrs/_libpathrs_cffi/__init__.pyi new file mode 100644 index 00000000..585873f4 --- /dev/null +++ b/contrib/bindings/python/pathrs/_libpathrs_cffi/__init__.pyi @@ -0,0 +1,19 @@ +# libpathrs: safe path resolution on Linux +# Copyright (C) 2019-2024 Aleksa Sarai +# Copyright (C) 2019-2024 SUSE LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cffi + +ffi: cffi.FFI diff --git a/contrib/bindings/python/pathrs/_libpathrs_cffi/lib.pyi b/contrib/bindings/python/pathrs/_libpathrs_cffi/lib.pyi new file mode 100644 index 00000000..3949a856 --- /dev/null +++ b/contrib/bindings/python/pathrs/_libpathrs_cffi/lib.pyi @@ -0,0 +1,83 @@ +# libpathrs: safe path resolution on Linux +# Copyright (C) 2019-2024 Aleksa Sarai +# Copyright (C) 2019-2024 SUSE LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cffi + +from typing import Optional, overload, type_check_only, Union + +# TODO: Remove this once we only support Python >= 3.10. +from typing_extensions import TypeAlias, Literal + +CBuffer: TypeAlias = cffi.FFI.CData # char[n] +CString: TypeAlias = cffi.FFI.CData # char * + +# pathrs_errorinfo_t * +@type_check_only +class CError: + saved_errno: int + description: CString + +ErrorId: TypeAlias = int +RawFd: TypeAlias = int + +# TODO: We actually return Union[CError, cffi.FFI.NULL] but we can't express +# this using the typing stubs for CFFI... +def pathrs_errorinfo(err_id: Union[ErrorId, int]) -> CError: ... +def pathrs_errorinfo_free(err: CError) -> None: ... + +ProcfsBase: TypeAlias = int + +PATHRS_PROC_ROOT: ProcfsBase +PATHRS_PROC_SELF: ProcfsBase +PATHRS_PROC_THREAD_SELF: ProcfsBase + +def pathrs_proc_open( + base: ProcfsBase, path: CString, flags: int +) -> Union[RawFd, ErrorId]: ... +def pathrs_proc_readlink( + base: ProcfsBase, path: CString, linkbuf: CBuffer, linkbuf_size: int +) -> Union[int, ErrorId]: ... +def pathrs_root_open(path: CString) -> Union[RawFd, ErrorId]: ... +def pathrs_reopen(fd: RawFd, flags: int) -> Union[RawFd, ErrorId]: ... +def pathrs_resolve(rootfd: RawFd, path: CString) -> Union[RawFd, ErrorId]: ... +def pathrs_resolve_nofollow(rootfd: RawFd, path: CString) -> Union[RawFd, ErrorId]: ... +def pathrs_creat( + rootfd: RawFd, path: CString, flags: int, filemode: int +) -> Union[RawFd, ErrorId]: ... +def pathrs_rename( + rootfd: RawFd, src: CString, dst: CString, flags: int +) -> Union[Literal[0], ErrorId]: ... +def pathrs_rmdir(rootfd: RawFd, path: CString) -> Union[Literal[0], ErrorId]: ... +def pathrs_unlink(rootfd: RawFd, path: CString) -> Union[Literal[0], ErrorId]: ... +def pathrs_remove_all(rootfd: RawFd, path: CString) -> Union[RawFd, ErrorId]: ... +def pathrs_mkdir( + rootfd: RawFd, path: CString, mode: int +) -> Union[Literal[0], ErrorId]: ... +def pathrs_mkdir_all( + rootfd: RawFd, path: CString, mode: int +) -> Union[Literal[0], ErrorId]: ... +def pathrs_mknod( + rootfd: RawFd, path: CString, mode: int, dev: int +) -> Union[Literal[0], ErrorId]: ... +def pathrs_hardlink( + rootfd: RawFd, path: CString, target: CString +) -> Union[Literal[0], ErrorId]: ... +def pathrs_symlink( + rootfd: RawFd, path: CString, target: CString +) -> Union[Literal[0], ErrorId]: ... +def pathrs_readlink( + rootfd: RawFd, path: CString, linkbuf: CBuffer, linkbuf_size: int +) -> Union[int, ErrorId]: ... diff --git a/contrib/bindings/python/pathrs/_pathrs.py b/contrib/bindings/python/pathrs/_pathrs.py index add9955d..1d07f5f8 100644 --- a/contrib/bindings/python/pathrs/_pathrs.py +++ b/contrib/bindings/python/pathrs/_pathrs.py @@ -23,7 +23,14 @@ import errno import fcntl +import typing +from typing import Any, IO, Optional, TextIO, Union +# TODO: Remove this once we only support Python >= 3.11. +from typing_extensions import Self + from ._libpathrs_cffi import ffi, lib as libpathrs_so +if typing.TYPE_CHECKING: + from ._libpathrs_cffi.lib import CBuffer, CString # type stubs __all__ = [ # core api @@ -35,13 +42,15 @@ "Error", ] -def _cstr(pystr): +def _cstr(pystr: str) -> "CString": return ffi.new("char[]", pystr.encode("utf8")) -def _pystr(cstr): - return ffi.string(cstr).decode("utf8") +def _pystr(cstr: "CString") -> str: + s = ffi.string(cstr) + assert isinstance(s, bytes) # typing + return s.decode("utf8") -def _cbuffer(size): +def _cbuffer(size: int) -> "CBuffer": return ffi.new("char[%d]" % (size,)) @@ -53,7 +62,11 @@ class Error(Exception): (Error.errno). """ - def __init__(self, message, *_, errno=None): + message: str + errno: Optional[int] + strerror: Optional[str] + + def __init__(self, message: str, *_, errno: Optional[int] = None): # Construct Exception. super().__init__(message) @@ -63,14 +76,14 @@ def __init__(self, message, *_, errno=None): # Pre-format the errno. self.strerror = None - if self.errno is not None: + if errno is not None: try: self.strerror = os.strerror(errno) except ValueError: self.strerror = str(errno) @classmethod - def _fetch(cls, err_id): + def _fetch(cls, err_id: int) -> Optional[Self]: if err_id >= 0: return None @@ -81,21 +94,22 @@ def _fetch(cls, err_id): description = _pystr(err.description) errno = err.saved_errno or None + # TODO: Should we use ffi.gc()? mypy doesn't seem to like our types... libpathrs_so.pathrs_errorinfo_free(err) del err return cls(description, errno=errno) - def __str__(self): + def __str__(self) -> str: if self.errno is None: return self.message else: return "%s (%s)" % (self.message, self.strerror) - def __repr__(self): + def __repr__(self) -> str: return "Error(%r, errno=%r)" % (self.message, self.errno) - def pprint(self, out=sys.stdout): + def pprint(self, out: TextIO = sys.stdout) -> None: "Pretty-print the error to the given @out file." # Basic error information. if self.errno is None: @@ -107,8 +121,13 @@ def pprint(self, out=sys.stdout): INTERNAL_ERROR = Error("tried to fetch libpathrs error but no error found") +class FilenoFile(typing.Protocol): + def fileno(self) -> int: + ... -def _fileno(file): +FileLike = Union[FilenoFile, int] + +def _fileno(file: FileLike) -> int: if isinstance(file, int): # file is a plain fd return file @@ -129,7 +148,9 @@ class WrappedFd(object): pathrs will return WrappedFds for most operations that return an fd. """ - def __init__(self, file): + _fd: Optional[int] + + def __init__(self, file: FileLike): """ Construct a WrappedFd from any file-like object. @@ -153,7 +174,7 @@ def __init__(self, file): fd = _clonefile(fd) self._fd = fd - def fileno(self): + def fileno(self) -> int: """ Return the file descriptor number of this WrappedFd. @@ -168,7 +189,7 @@ def fileno(self): raise OSError(errno.EBADF, "Closed file descriptor") return self._fd - def leak(self): + def leak(self) -> None: """ Clears this WrappedFd without closing the underlying file, to stop GC from closing the file. @@ -180,7 +201,7 @@ def leak(self): """ self._fd = None - def fdopen(self, mode="r"): + def fdopen(self, mode: str = "r") -> IO[Any]: """ Convert this WrappedFd into an os.fileopen() handle. @@ -199,11 +220,11 @@ def fdopen(self, mode="r"): raise @classmethod - def from_raw_fd(cls, fd): + def from_raw_fd(cls, fd: int) -> Self: "Shorthand for WrappedFd(fd)." return cls(fd) - def into_raw_fd(self): + def into_raw_fd(self) -> int: """ Convert this WrappedFd into a raw file descriptor that GC won't touch. @@ -214,14 +235,14 @@ def into_raw_fd(self): self.leak() return fd - def isclosed(self): + def isclosed(self) -> bool: """ Returns whether the underlying file descriptor is closed or the WrappedFd has been leaked. """ return self._fd is None - def close(self): + def close(self) -> None: """ Manually close the underlying file descriptor for this WrappedFd. @@ -229,59 +250,61 @@ def close(self): you really care about the point where a file is closed. """ if not self.isclosed(): + assert self._fd is not None # typing os.close(self._fd) self._fd = None - def clone(self): + def clone(self) -> Self: "Create a clone of this WrappedFd that has a separate lifetime." if self.isclosed(): raise ValueError("cannot clone closed file") - return self.__class__(_clonefile(self)) + assert self._fd is not None # typing + return self.__class__(_clonefile(self._fd)) - def __copy__(self): + def __copy__(self) -> Self: "Identical to WrappedFd.clone()" # A "shallow copy" of a file is the same as a deep copy. return copy.deepcopy(self) - def __deepcopy__(self, memo): + def __deepcopy__(self, memo) -> Self: "Identical to WrappedFd.clone()" return self.clone() - def __del__(self): + def __del__(self) -> None: "Identical to WrappedFd.close()" self.close() - def __enter__(self): + def __enter__(self) -> Self: return self - def __exit__(self, exc_type, exc_value, exc_traceback): + def __exit__(self, exc_type, exc_value, exc_traceback) -> None: self.close() # XXX: This is _super_ ugly but so is the one in CPython. -def _convert_mode(mode): - mode = set(mode) +def _convert_mode(mode: str) -> int: + mode_set = set(mode) flags = os.O_CLOEXEC # We don't support O_CREAT or O_EXCL with libpathrs -- use creat(). - if "x" in mode: + if "x" in mode_set: raise ValueError("pathrs doesn't support mode='x', use Root.creat()") # Basic sanity-check to make sure we don't accept garbage modes. - if len(mode & {"r", "w", "a"}) > 1: + if len(mode_set & {"r", "w", "a"}) > 1: raise ValueError("must have exactly one of read/write/append mode") read = False write = False - if "+" in mode: + if "+" in mode_set: read = True write = True - if "r" in mode: + if "r" in mode_set: read = True - if "w" in mode: + if "w" in mode_set: write = True flags |= os.O_TRUNC - if "a" in mode: + if "a" in mode_set: write = True flags |= os.O_APPEND @@ -296,11 +319,22 @@ def _convert_mode(mode): return flags +#: Resolve proc_* operations relative to the /proc root. Note that this mode +#: may be more expensive because we have to take steps to try to avoid leaking +#: unmasked procfs handles, so you should use PROC_SELF if you can. PROC_ROOT = libpathrs_so.PATHRS_PROC_ROOT + +#: Resolve proc_* operations relative to /proc/self. For most programs, this is +#: the standard choice. PROC_SELF = libpathrs_so.PATHRS_PROC_SELF + +#: Resolve proc_* operations relative to /proc/thread-self. In multi-threaded +#: programs where one thread has a different CLONE_FS, it is possible for +#: /proc/self to point the wrong thread and so /proc/thread-self may be +#: necessary. PROC_THREAD_SELF = libpathrs_so.PATHRS_PROC_THREAD_SELF -def proc_open(base, path, mode="r", extra_flags=0): +def proc_open(base, path: str, mode: str = "r", extra_flags: int = 0): """ Open a procfs file using Pythonic mode strings. @@ -322,7 +356,7 @@ def proc_open(base, path, mode="r", extra_flags=0): with proc_open_raw(base, path, flags) as file: return file.fdopen(mode) -def proc_open_raw(base, path, flags): +def proc_open_raw(base, path: str, flags: int) -> WrappedFd: """ Open a procfs file using Unix open flags. @@ -339,13 +373,12 @@ def proc_open_raw(base, path, flags): let libpathrs know that it can be more strict when opening the path. """ # TODO: Should we default to O_NOFOLLOW or put a separate argument for it? - path = _cstr(path) - fd = libpathrs_so.pathrs_proc_open(base, path, flags) + fd = libpathrs_so.pathrs_proc_open(base, _cstr(path), flags) if fd < 0: raise Error._fetch(fd) or INTERNAL_ERROR return WrappedFd(fd) -def proc_readlink(base, path): +def proc_readlink(base, path: str) -> str: """ Fetch the target of a procfs symlink. @@ -359,11 +392,11 @@ def proc_readlink(base, path): open. """ # TODO: See if we can merge this with Root.readlink. - path = _cstr(path) + cpath = _cstr(path) linkbuf_size = 128 while True: linkbuf = _cbuffer(linkbuf_size) - n = libpathrs_so.pathrs_proc_readlink(base, path, linkbuf, linkbuf_size) + n = libpathrs_so.pathrs_proc_readlink(base, cpath, linkbuf, linkbuf_size) if n < 0: raise Error._fetch(n) or INTERNAL_ERROR elif n <= linkbuf_size: @@ -380,16 +413,16 @@ def proc_readlink(base, path): class Handle(WrappedFd): "A handle to a filesystem object, usually resolved using Root.resolve()." - def __init__(self, file): + def __init__(self, file: FileLike): # XXX: Is this necessary? super().__init__(file) @classmethod - def from_file(cls, file): + def from_file(cls, file: FileLike): "Manually create a Handle from a file-like object." return cls(file) - def reopen(self, mode="r", extra_flags=0): + def reopen(self, mode: str = "r", extra_flags: int = 0) -> IO[Any]: """ Upgrade a Handle to a os.fdopen() file handle. @@ -403,7 +436,7 @@ def reopen(self, mode="r", extra_flags=0): with self.reopen_raw(flags) as file: return file.fdopen(mode) - def reopen_raw(self, flags): + def reopen_raw(self, flags: int) -> WrappedFd: """ Upgrade a Handle to a WrappedFd file handle. @@ -424,7 +457,7 @@ class Root(WrappedFd): relative to. """ - def __init__(self, file_or_path): + def __init__(self, file_or_path: Union[FileLike, str]): """ Create a handle from a file-like object or a path to a directory. @@ -433,27 +466,29 @@ def __init__(self, file_or_path): is a path string, be aware there are no protections against rename race attacks when opening the Root directory handle itself. """ - file = file_or_path if isinstance(file_or_path, str): path = _cstr(file_or_path) fd = libpathrs_so.pathrs_root_open(path) if fd < 0: raise Error._fetch(fd) or INTERNAL_ERROR - file = fd + file: FileLike = fd + else: + file = file_or_path + # XXX: Is this necessary? super().__init__(file) @classmethod - def open(cls, path): + def open(cls, path: str) -> Self: "Identical to Root(path)." return cls(path) @classmethod - def from_file(cls, file): + def from_file(cls, file: FileLike) -> Self: "Identical to Root(file)." return cls(file) - def resolve(self, path, follow_trailing=True): + def resolve(self, path: str, follow_trailing: bool = True) -> Handle: """ Resolve the given path inside the Root and return a Handle. @@ -464,26 +499,25 @@ def resolve(self, path, follow_trailing=True): A pathrs.Error is raised if the path doesn't exist. """ - path = _cstr(path) if follow_trailing: - fd = libpathrs_so.pathrs_resolve(self.fileno(), path) + fd = libpathrs_so.pathrs_resolve(self.fileno(), _cstr(path)) else: - fd = libpathrs_so.pathrs_resolve_nofollow(self.fileno(), path) + fd = libpathrs_so.pathrs_resolve_nofollow(self.fileno(), _cstr(path)) if fd < 0: raise Error._fetch(fd) or INTERNAL_ERROR return Handle(fd) - def readlink(self, path): + def readlink(self, path: str) -> str: """ Fetch the target of a symlink at the given path in the Root. A pathrs.Error is raised if the path is not a symlink or doesn't exist. """ - path = _cstr(path) + cpath = _cstr(path) linkbuf_size = 128 while True: linkbuf = _cbuffer(linkbuf_size) - n = libpathrs_so.pathrs_readlink(self.fileno(), path, linkbuf, linkbuf_size) + n = libpathrs_so.pathrs_readlink(self.fileno(), cpath, linkbuf, linkbuf_size) if n < 0: raise Error._fetch(n) or INTERNAL_ERROR elif n <= linkbuf_size: @@ -496,12 +530,12 @@ def readlink(self, path): # make sure we are a fair bit larger). linkbuf_size += n - def creat(self, path, filemode, mode="r", extra_flags=0): + def creat(self, path: str, filemode: int, mode: str = "r", extra_flags: int = 0) -> IO[Any]: """ Atomically create-and-open a new file at the given path in the Root, a-la O_CREAT. - This method returns a Handle. + This method returns an os.fdopen() file handle. filemode is the Unix DAC mode you wish the new file to be created with. This mode might not be the actual mode of the created file due to a @@ -512,16 +546,15 @@ def creat(self, path, filemode, mode="r", extra_flags=0): to ensure the new file was created *by you* then you may wish to add O_EXCL to extra_flags. """ - path = _cstr(path) flags = _convert_mode(mode) | extra_flags - fd = libpathrs_so.pathrs_creat(self.fileno(), path, flags, filemode) + fd = libpathrs_so.pathrs_creat(self.fileno(), _cstr(path), flags, filemode) if fd < 0: raise Error._fetch(fd) or INTERNAL_ERROR return os.fdopen(fd, mode) # TODO: creat_raw? - def rename(self, src, dst, flags=0): + def rename(self, src: str, dst: str, flags: int = 0) -> None: """ Rename a path from src to dst within the Root. @@ -530,25 +563,22 @@ def rename(self, src, dst, flags=0): RENAME_EXCHANGE will turn this into an atomic swap operation. """ # TODO: Should we have a separate Root.swap() operation? - src = _cstr(src) - dst = _cstr(dst) - err = libpathrs_so.pathrs_rename(self.fileno(), src, dst, flags) + err = libpathrs_so.pathrs_rename(self.fileno(), _cstr(src), _cstr(dst), flags) if err < 0: raise Error._fetch(err) or INTERNAL_ERROR - def rmdir(self, path): + def rmdir(self, path: str) -> None: """ Remove an empty directory at the given path within the Root. To remove non-empty directories recursively, you can use Root.remove_all(). """ - path = _cstr(path) - err = libpathrs_so.pathrs_rmdir(self.fileno(), path) + err = libpathrs_so.pathrs_rmdir(self.fileno(), _cstr(path)) if err < 0: raise Error._fetch(err) or INTERNAL_ERROR - def unlink(self, path): + def unlink(self, path: str) -> None: """ Remove a non-directory inode at the given path within the Root. @@ -556,22 +586,20 @@ def unlink(self, path): files and non-empty directories recursively, you can use Root.remove_all(). """ - path = _cstr(path) - err = libpathrs_so.pathrs_unlink(self.fileno(), path) + err = libpathrs_so.pathrs_unlink(self.fileno(), _cstr(path)) if err < 0: raise Error._fetch(err) or INTERNAL_ERROR - def remove_all(self, path): + def remove_all(self, path: str) -> None: """ Remove the file or directory (empty or non-empty) at the given path within the Root. """ - path = _cstr(path) - err = libpathrs_so.pathrs_remove_all(self.fileno(), path) + err = libpathrs_so.pathrs_remove_all(self.fileno(), _cstr(path)) if err < 0: raise Error._fetch(err) or INTERNAL_ERROR - def mkdir(self, path, mode): + def mkdir(self, path: str, mode: int) -> None: """ Create a directory at the given path within the Root. @@ -584,12 +612,11 @@ def mkdir(self, path, mode): directories (or just re-use an existing directory) you can use Root.mkdir_all(). """ - path = _cstr(path) - err = libpathrs_so.pathrs_mkdir(self.fileno(), path, mode) + err = libpathrs_so.pathrs_mkdir(self.fileno(), _cstr(path), mode) if err < 0: raise Error._fetch(err) or INTERNAL_ERROR - def mkdir_all(self, path, mode): + def mkdir_all(self, path: str, mode: int) -> Handle: """ Recursively create a directory and all of its parents at the given path within the Root (or re-use an existing directory if the path already @@ -603,13 +630,12 @@ def mkdir_all(self, path, mode): full path already exists, this mode is ignored and the existing directory mode is kept. """ - path = _cstr(path) - fd = libpathrs_so.pathrs_mkdir_all(self.fileno(), path, mode) + fd = libpathrs_so.pathrs_mkdir_all(self.fileno(), _cstr(path), mode) if fd < 0: raise Error._fetch(fd) or INTERNAL_ERROR return Handle(fd) - def mknod(self, path, mode, dev=0): + def mknod(self, path: str, mode: int, dev: int = 0) -> None: """ Create a new inode at the given path within the Root. @@ -625,12 +651,11 @@ def mknod(self, path, mode, dev=0): A pathrs.Error is raised if the path already exists. """ - path = _cstr(path) - err = libpathrs_so.pathrs_mknod(self.fileno(), path, mode, dev) + err = libpathrs_so.pathrs_mknod(self.fileno(), _cstr(path), mode, dev) if err < 0: raise Error._fetch(err) or INTERNAL_ERROR - def hardlink(self, path, target): + def hardlink(self, path: str, target: str) -> None: """ Create a hardlink between two paths inside the Root. @@ -640,13 +665,11 @@ def hardlink(self, path, target): A pathrs.Error is raised if the path for the new hardlink already exists. """ - path = _cstr(path) - target = _cstr(target) - err = libpathrs_so.pathrs_hardlink(self.fileno(), path, target) + err = libpathrs_so.pathrs_hardlink(self.fileno(), _cstr(path), _cstr(target)) if err < 0: raise Error._fetch(err) or INTERNAL_ERROR - def symlink(self, path, target): + def symlink(self, path: str, target: str) -> None: """ Create a symlink at the given path in the Root. @@ -657,8 +680,6 @@ def symlink(self, path, target): A pathrs.Error is raised if the path for the new symlink already exists. """ - path = _cstr(path) - target = _cstr(target) - err = libpathrs_so.pathrs_symlink(self.fileno(), path, target) + err = libpathrs_so.pathrs_symlink(self.fileno(), _cstr(path), _cstr(target)) if err < 0: raise Error._fetch(err) or INTERNAL_ERROR diff --git a/contrib/bindings/python/pathrs/pathrs_build.py b/contrib/bindings/python/pathrs/pathrs_build.py index 65f0a932..526bb981 100755 --- a/contrib/bindings/python/pathrs/pathrs_build.py +++ b/contrib/bindings/python/pathrs/pathrs_build.py @@ -23,9 +23,12 @@ import os import sys +from typing import Optional +from collections.abc import Iterable + import cffi -def load_hdr(ffi, hdr_path): +def load_hdr(ffi: cffi.FFI, hdr_path: str) -> None: with open(hdr_path) as f: hdr = f.read() @@ -41,7 +44,7 @@ def load_hdr(ffi, hdr_path): # Load the header. ffi.cdef(hdr) -def create_ffibuilder(**kwargs): +def create_ffibuilder(**kwargs) -> cffi.FFI: ffibuilder = cffi.FFI() ffibuilder.cdef("typedef uint32_t dev_t;") @@ -58,7 +61,7 @@ def create_ffibuilder(**kwargs): return ffibuilder -def find_rootdir(): +def find_rootdir() -> str: # Figure out where the libpathrs source dir is. root_dir = None candidate = os.path.dirname(sys.path[0] or os.getcwd()) @@ -80,7 +83,7 @@ def find_rootdir(): return root_dir -def srcdir_ffibuilder(root_dir=None): +def srcdir_ffibuilder(root_dir: Optional[str] = None) -> cffi.FFI: """ Build the CFFI bindings using the provided root_dir as the root of a pathrs source tree which has compiled cdylibs ready in target/*. @@ -90,7 +93,7 @@ def srcdir_ffibuilder(root_dir=None): root_dir = find_rootdir() # Figure out which libs are usable. - library_dirs = ( + library_dirs: Iterable[str] = ( os.path.join(root_dir, "target/%s/libpathrs.so" % (mode,)) for mode in ("debug", "release") ) @@ -102,7 +105,7 @@ def srcdir_ffibuilder(root_dir=None): return create_ffibuilder(include_dirs=[os.path.join(root_dir, "include")], library_dirs=library_dirs) -def system_ffibuilder(): +def system_ffibuilder() -> cffi.FFI: """ Build the CFFI bindings using the installed libpathrs system libraries. """ diff --git a/contrib/bindings/python/pathrs/py.typed b/contrib/bindings/python/pathrs/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/contrib/bindings/python/pyproject.toml b/contrib/bindings/python/pyproject.toml index 240a090d..9d1c9fd2 100644 --- a/contrib/bindings/python/pyproject.toml +++ b/contrib/bindings/python/pyproject.toml @@ -53,6 +53,7 @@ classifiers = [ requires-python = ">= 3.8" dependencies = [ "cffi>=1.10.0", + "typing_extensions>=4.0.0", # TODO: Remove this once we only support Python >= 3.11. ] [project.urls] From 8e27c044b5e84df3678a5ec79405b40e3e387c4b Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Mon, 7 Oct 2024 23:12:41 +1100 Subject: [PATCH 6/8] python bindings: make type annotations for CString and CBuffer less magical In order to avoid having magical types that only exist during type-checking, define CString and CBytes in the main _pathrs module and import them into the _libpathrs_cffi type stubs. Unfortunately, the straight-forward way of doing this (using a TypeAlias of _libpathrs_cffi.ffi.CData) doesn't work for some reason so we need to do a little bit of magic when TYPE_CHECKING to make mypy happy. But at least this is a little bit less magical than the previous version (where some variables only existed when type-checking). Signed-off-by: Aleksa Sarai --- .../python/pathrs/_libpathrs_cffi/lib.pyi | 3 +-- contrib/bindings/python/pathrs/_pathrs.py | 23 ++++++++++++++----- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/contrib/bindings/python/pathrs/_libpathrs_cffi/lib.pyi b/contrib/bindings/python/pathrs/_libpathrs_cffi/lib.pyi index 3949a856..e28521be 100644 --- a/contrib/bindings/python/pathrs/_libpathrs_cffi/lib.pyi +++ b/contrib/bindings/python/pathrs/_libpathrs_cffi/lib.pyi @@ -21,8 +21,7 @@ from typing import Optional, overload, type_check_only, Union # TODO: Remove this once we only support Python >= 3.10. from typing_extensions import TypeAlias, Literal -CBuffer: TypeAlias = cffi.FFI.CData # char[n] -CString: TypeAlias = cffi.FFI.CData # char * +from .._pathrs import CBuffer, CString # pathrs_errorinfo_t * @type_check_only diff --git a/contrib/bindings/python/pathrs/_pathrs.py b/contrib/bindings/python/pathrs/_pathrs.py index 1d07f5f8..0eff1f49 100644 --- a/contrib/bindings/python/pathrs/_pathrs.py +++ b/contrib/bindings/python/pathrs/_pathrs.py @@ -26,11 +26,22 @@ import typing from typing import Any, IO, Optional, TextIO, Union # TODO: Remove this once we only support Python >= 3.11. -from typing_extensions import Self +from typing_extensions import Self, TypeAlias -from ._libpathrs_cffi import ffi, lib as libpathrs_so if typing.TYPE_CHECKING: - from ._libpathrs_cffi.lib import CBuffer, CString # type stubs + # mypy apparently cannot handle the "ffi: cffi.api.FFI" definition in + # _libpathrs_cffi/__init__.pyi so we need to explicitly reference the type + # from cffi here. + import cffi + ffi: cffi.FFI + CString: TypeAlias = cffi.FFI.CData + CBuffer: TypeAlias = cffi.FFI.CData +else: + from ._libpathrs_cffi import ffi + CString: TypeAlias = ffi.CData + CBuffer: TypeAlias = ffi.CData + +from ._libpathrs_cffi import lib as libpathrs_so __all__ = [ # core api @@ -42,15 +53,15 @@ "Error", ] -def _cstr(pystr: str) -> "CString": +def _cstr(pystr: str) -> CString: return ffi.new("char[]", pystr.encode("utf8")) -def _pystr(cstr: "CString") -> str: +def _pystr(cstr: CString) -> str: s = ffi.string(cstr) assert isinstance(s, bytes) # typing return s.decode("utf8") -def _cbuffer(size: int) -> "CBuffer": +def _cbuffer(size: int) -> CBuffer: return ffi.new("char[%d]" % (size,)) From 68f93ed56d818fdf02b02cb0372647e54b67a81a Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Tue, 8 Oct 2024 14:55:09 +1100 Subject: [PATCH 7/8] gha: split bindings-ci into per-language actions This will make it easier to add per-language lints and checks. Signed-off-by: Aleksa Sarai --- .github/workflows/bindings-c.yml | 48 +++++++++++++++ .github/workflows/bindings-go.yml | 58 +++++++++++++++++++ .../{bindings.yml => bindings-python.yml} | 47 +++------------ .github/workflows/rust.yml | 13 +++++ 4 files changed, 128 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/bindings-c.yml create mode 100644 .github/workflows/bindings-go.yml rename .github/workflows/{bindings.yml => bindings-python.yml} (73%) diff --git a/.github/workflows/bindings-c.yml b/.github/workflows/bindings-c.yml new file mode 100644 index 00000000..dc4b3443 --- /dev/null +++ b/.github/workflows/bindings-c.yml @@ -0,0 +1,48 @@ +# libpathrs: safe path resolution on Linux +# Copyright (C) 2019-2024 Aleksa Sarai +# Copyright (C) 2019-2024 SUSE LLC +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program 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 Lesser General Public License along +# with this program. If not, see . + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + release: + types: [ published ] + schedule: + - cron: '0 0 * * *' + +name: bindings-c + +jobs: + smoke-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Build and install libpathrs.so. + - uses: dtolnay/rust-toolchain@stable + - name: build libpathrs + run: make release + - name: install libpathrs + run: sudo ./install.sh --libdir=/usr/lib + # Run smoke-tests. + - run: make -C examples/c smoke-test + + complete: + needs: + - smoke-test + runs-on: ubuntu-latest + steps: + - run: echo "C CI jobs completed successfully." diff --git a/.github/workflows/bindings-go.yml b/.github/workflows/bindings-go.yml new file mode 100644 index 00000000..f90d90e0 --- /dev/null +++ b/.github/workflows/bindings-go.yml @@ -0,0 +1,58 @@ +# libpathrs: safe path resolution on Linux +# Copyright (C) 2019-2024 Aleksa Sarai +# Copyright (C) 2019-2024 SUSE LLC +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program 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 Lesser General Public License along +# with this program. If not, see . + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + release: + types: [ published ] + schedule: + - cron: '0 0 * * *' + +name: bindings-go + +jobs: + smoke-test: + strategy: + fail-fast: false + matrix: + go-version: ["1.18.x", "1.21.x", "1.22.x"] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Build and install libpathrs.so. + - uses: dtolnay/rust-toolchain@stable + - name: build libpathrs + run: make release + - name: install libpathrs + run: sudo ./install.sh --libdir=/usr/lib + # Setup go. + - name: install go ${{ matrix.go-version }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + check-latest: true + # Run smoke-tests. + - run: make -C examples/go smoke-test + + complete: + needs: + - smoke-test + runs-on: ubuntu-latest + steps: + - run: echo "Go CI jobs completed successfully." diff --git a/.github/workflows/bindings.yml b/.github/workflows/bindings-python.yml similarity index 73% rename from .github/workflows/bindings.yml rename to .github/workflows/bindings-python.yml index 7b8e1aa6..9d49cd90 100644 --- a/.github/workflows/bindings.yml +++ b/.github/workflows/bindings-python.yml @@ -24,46 +24,10 @@ on: schedule: - cron: '0 0 * * *' -name: bindings-ci +name: bindings-python jobs: - c: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - # Build and install libpathrs.so. - - uses: dtolnay/rust-toolchain@stable - - name: build libpathrs - run: make release - - name: install libpathrs - run: sudo ./install.sh --libdir=/usr/lib - # Run smoke-tests. - - run: make -C examples/c smoke-test - - go: - strategy: - fail-fast: false - matrix: - go-version: ["1.18.x", "1.21.x", "1.22.x"] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - # Build and install libpathrs.so. - - uses: dtolnay/rust-toolchain@stable - - name: build libpathrs - run: make release - - name: install libpathrs - run: sudo ./install.sh --libdir=/usr/lib - # Setup go. - - name: install go ${{ matrix.go-version }} - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - check-latest: true - # Run smoke-tests. - - run: make -C examples/go smoke-test - - python: + smoke-test: strategy: fail-fast: false matrix: @@ -108,3 +72,10 @@ jobs: path: contrib/bindings/python/dist/ # Run smoke-tests. - run: make -C examples/python smoke-test + + complete: + needs: + - smoke-test + runs-on: ubuntu-latest + steps: + - run: echo "Python CI jobs completed successfully." diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index caac28f1..92736d5f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -117,3 +117,16 @@ jobs: with: components: clippy - run: cargo clippy --all-features --all-targets + + complete: + needs: + - check + - check-msrv + - rustdoc + - test + - examples + - fmt + - clippy + runs-on: ubuntu-latest + steps: + - run: echo "Rust CI jobs completed successfully." From ffab0c0b1871295251a381eb33c526fd033f5cf3 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Tue, 8 Oct 2024 12:40:10 +1100 Subject: [PATCH 8/8] gha: add python mypy checking Signed-off-by: Aleksa Sarai --- .github/workflows/bindings-python.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/bindings-python.yml b/.github/workflows/bindings-python.yml index 9d49cd90..148c174c 100644 --- a/.github/workflows/bindings-python.yml +++ b/.github/workflows/bindings-python.yml @@ -27,6 +27,28 @@ on: name: bindings-python jobs: + # TODO: Do some kind of lints? + + mypy: + permissions: + contents: read + pull-requests: read + checks: write # allow the action to annotate code + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Set up python venv. + - uses: actions/setup-python@v5 + - name: install mypy + run: >- + python3 -m pip install --user mypy + - uses: tsuyoshicho/action-mypy@v4 + with: + github_token: ${{ secrets.github_token }} + reporter: github-check + workdir: contrib/bindings/python/pathrs + fail_on_error: true + smoke-test: strategy: fail-fast: false @@ -75,6 +97,7 @@ jobs: complete: needs: + - mypy - smoke-test runs-on: ubuntu-latest steps: