diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/make.py b/make.py index 28adfb0..d0de3b6 100644 --- a/make.py +++ b/make.py @@ -1,6 +1,6 @@ #! python3 -from umake.umake import make, Config +from umake import make, Config def main(): diff --git a/makefile b/makefile index 4d5ae1c..2fed85b 100644 --- a/makefile +++ b/makefile @@ -1,13 +1,12 @@ -MAKE_PATH="." UMAKE_PATH="umake" .PHONY: all debug clean all: - @PYTHONPATH=$(UMAKE_PATH) python3 $(MAKE_PATH)/make.py + @PYTHONPATH=$(UMAKE_PATH) python3 make.py debug: - @PYTHONPATH=$(UMAKE_PATH) python3 $(MAKE_PATH)/make.py --type debug + @PYTHONPATH=$(UMAKE_PATH) python3 make.py --type debug clean: - @PYTHONPATH=$(UMAKE_PATH) python3 $(MAKE_PATH)/make.py --clean + @PYTHONPATH=$(UMAKE_PATH) python3 make.py --clean diff --git a/pyproject.toml b/pyproject.toml index 740e13e..06b5939 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,9 @@ name = 'umake' version = '1.0.0' requires-python = ">=3.8" +[project.optional-dependencies] +watch = ["watchinotify==0.2"] + [tool.ruff] cache-dir = "/tmp/ruff" exclude = [ @@ -51,6 +54,3 @@ lines_after_imports = 2 multi_line_output = 4 no_sections = false order_by_type = true - -[tool.pyright] -exclude = ["**/__pycache__", "**/.venv", "**/services", "web_front"] diff --git a/umake/__init__.py b/umake/__init__.py new file mode 100644 index 0000000..34d6841 --- /dev/null +++ b/umake/__init__.py @@ -0,0 +1,3 @@ +from .core import get_hash, make, Config + +__all__ = ['get_hash', 'make', 'Config'] diff --git a/umake.py b/umake/core.py similarity index 94% rename from umake.py rename to umake/core.py index 50a3c41..f804b3b 100755 --- a/umake.py +++ b/umake/core.py @@ -135,25 +135,30 @@ class Builder: self._populate_todo_dict() if self._config.watch: - from watch import Watcher, WatchFlag # ruff:ignore[import-outside-top-level] + from watchinotify import Event, FileEvent, FolderEvent, Watcher # ruff:ignore[import-outside-top-level] - def callback(path: Path, _: WatchFlag): + self._compile_sources() + watcher = Watcher() + + def callback(path: Path | None, event: Event, _name: bytes): + print(f'File changed {path=} {event=}') + if path is None: + return source_hash = get_hash(path) if path not in self._hash_dict or source_hash != self._hash_dict[path]: print(f'{ConsoleColor.ORANGE}Source file changed : {path} {ConsoleColor.ENDCOLOR}') self._populate_todo_dict() self._compile_sources() - print(f'{ConsoleColor.GREEN}Watching for changes{ConsoleColor.ENDCOLOR}') + print(f'{ConsoleColor.GREEN}Watching for changes ({len(watcher.watched_paths())} files){ConsoleColor.ENDCOLOR}') - self._compile_sources() - print(f'{ConsoleColor.GREEN}Watching for changes{ConsoleColor.ENDCOLOR}') - watcher = Watcher() - for source_path in self._compile_dict: - watcher.register(source_path, WatchFlag.MODIFY, callback) - try: - watcher.watch() - except KeyboardInterrupt: - print('\rExit') + watcher.callback = callback + with watcher: + watcher.watch(self._hash_dict) + print(f'{ConsoleColor.GREEN}Watching for changes ({len(watcher.watched_paths())} files){ConsoleColor.ENDCOLOR}') + try: + watcher._thread.join() + except KeyboardInterrupt: + print('\rExit') elif not self._compile_sources(): sys.exit(1) @@ -191,7 +196,7 @@ class Builder: return True # Running compilation processes - if not self._config.object_dir.exists: + if not self._config.object_dir.exists(): self._config.object_dir.mkdir(parents=True) error_paths: list[tuple[Path, str]] = [] if self._config.job_count > 1: # Multi-process diff --git a/watch.py b/watch.py deleted file mode 100644 index 250b4c8..0000000 --- a/watch.py +++ /dev/null @@ -1,151 +0,0 @@ -import ctypes -import ctypes.util -from enum import IntFlag -import os -from pathlib import Path -import select -from struct import unpack, calcsize -from typing import Callable - - -class WatchFlag(IntFlag): - # Supported events suitable for MASK parameter of INOTIFY_ADD_WATCH. - ACCESS = 0x00000001 # File was accessed - MODIFY = 0x00000002 # File was modified - ATTRIB = 0x00000004 # Metadata changed - CLOSE_WRITE = 0x00000008 # Writtable file was closed - CLOSE_NOWRITE = 0x00000010 # Unwrittable file closed - # CLOSE = (CLOSE_WRITE | CLOSE_NOWRITE) # Close - OPEN = 0x00000020 # File was opened - MOVED_FROM = 0x00000040 # File was moved from X - MOVED_TO = 0x00000080 # File was moved to Y - # MOVE = (MOVED_FROM | MOVED_TO) # Moves - CREATE = 0x00000100 # Subfile was created - DELETE = 0x00000200 # Subfile was deleted - DELETE_SELF = 0x00000400 # Self was deleted - MOVE_SELF = 0x00000800 # Self was moved - - # Events sent by the kernel. - UNMOUNT = 0x00002000 # Backing fs was unmounted - Q_OVERFLOW = 0x00004000 # Event queued overflowed - IGNORED = 0x00008000 # File was ignored - - # Helper events. - # CLOSE = (CLOSE_WRITE | CLOSE_NOWRITE) # Close - # MOVE = (MOVED_FROM | MOVED_TO) # Moves - - # Special flags. - ONLYDIR = 0x01000000 # Only watch the path if it is a directory - DONT_FOLLOW = 0x02000000 # Do not follow a sym link - EXCL_UNLINK = 0x04000000 # Exclude events on unlinked objects - MASK_CREATE = 0x10000000 # Only create watches - MASK_ADD = 0x20000000 # Add to the mask of an already existing watch - ISDIR = 0x40000000 # Event occurred against dir - ONESHOT = 0x80000000 # Only send event once - - # All events which a program can wait on. - # ALL_EVENTS = ( - # ACCESS | MODIFY | ATTRIB | CLOSE_WRITE | CLOSE_NOWRITE | OPEN | MOVED_FROM | MOVED_TO - # | CREATE | DELETE | DELETE_SELF | MOVE_SELF) - - -class InotifyError(Exception): - def __init__(self, message, *args, **kwargs): - message += f' ERRNO=({ctypes.get_errno()})' - super().__init__(message, *args, **kwargs) - - -WatcherCallback = Callable[[Path, WatchFlag], None] - - -class Watcher: - _EVENT_FORMAT = 'iIII' - _EVENT_SIZE = calcsize(_EVENT_FORMAT) - - def __init__(self): - libc_path = ctypes.util.find_library('c') - if libc_path is None: - libc_path = 'libc.so.6' - self._instance = ctypes.cdll.LoadLibrary(libc_path) - - self._inotify_init = self._instance.inotify_init - self._inotify_init.argtypes = [] - self._inotify_init.restype = self._check_nonnegative - - self._inotify_add_watch = self._instance.inotify_add_watch - self._inotify_add_watch.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32] - self._inotify_add_watch.restype = self._check_nonnegative - - self._inotify_rm_watch = self._instance.inotify_rm_watch - self._inotify_rm_watch.argtypes = [ctypes.c_int, ctypes.c_int] - self._inotify_rm_watch.restype = self._check_nonnegative - - self._inotify_fd = self._inotify_init() - - self._poll = select.poll() - self._poll.register(self._inotify_fd, select.POLLIN) - - self._watch_fds: dict[Path, int] = {} - self._watch_info: dict[int, tuple[Path, WatcherCallback]] = {} - - def register(self, path: Path, flags: WatchFlag, callback: WatcherCallback): - if path in self._watch_fds: - self._inotify_rm_watch(self._inotify_fd, self._watch_fds[path]) - - watch_fd = self._inotify_add_watch(self._inotify_fd, str(path).encode(), flags) - self._watch_fds[path] = watch_fd - self._watch_info[watch_fd] = (path, callback) - - def unregister(self, path: Path): - watch_fd = self._watch_fds[path] - self._inotify_rm_watch(self._inotify_fd, watch_fd) - del self._watch_fds[path] - del self._watch_info[watch_fd] - - def watch(self): - while True: - results = self._poll.poll() - for poll_fd, _ in results: - watch_buffer = os.read(poll_fd, self._EVENT_SIZE) - if len(watch_buffer) < self._EVENT_SIZE: - continue - watch_fd, mask, _, _ = unpack(self._EVENT_FORMAT, watch_buffer) - watch_path, callback = self._watch_info[watch_fd] - callback(watch_path, WatchFlag(mask)) - - def __del__(self): - self._poll.unregister(self._inotify_fd) - for watch_fd in self._watch_fds.values(): - self._inotify_rm_watch(self._inotify_fd, watch_fd) - os.close(self._inotify_fd) - - @staticmethod - def _check_nonnegative(result): - if result == -1: - raise InotifyError(f'Call failed (should not be -1): {result}') - return result - - -def main(): - from argparse import ArgumentParser # ruff:ignore[import-outside-top-level] - - parser = ArgumentParser() - parser.add_argument('paths', nargs='*', type=Path) - arguments = parser.parse_args() - - paths: list[Path] = arguments.paths - - def callback(path: Path, _: WatchFlag): - print(f'{path} has been modified') - - watcher = Watcher() - for path in paths: - watcher.register(path, WatchFlag.MODIFY, callback) - try: - watcher.watch() - except KeyboardInterrupt: - print('\rExit') - - -if __name__ == '__main__': - main()