Change layout and use watchinotify

* Change code layout to a flat layout
* Remove pyright configuration
* Add watchinotify as optional dependency to use the watch option
* Fix watch option to watch for all dependencies (headers)
This commit is contained in:
Corentin 2026-07-24 01:04:21 +09:00
commit b31e3b8426
Signed by: corentin
GPG key ID: 48C87E27C6C917F4
7 changed files with 29 additions and 173 deletions

View file

View file

@ -1,6 +1,6 @@
#! python3
from umake.umake import make, Config
from umake import make, Config
def main():

View file

@ -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

View file

@ -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"]

3
umake/__init__.py Normal file
View file

@ -0,0 +1,3 @@
from .core import get_hash, make, Config
__all__ = ['get_hash', 'make', 'Config']

View file

@ -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

151
watch.py
View file

@ -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()