From: Thomas Walker Lynch Date: Fri, 9 Jan 2026 09:27:11 +0000 (+0000) Subject: tweaks to structure X-Git-Url: https://git.reasoningtechnology.com/?a=commitdiff_plain;h=13d3ebcb8c3894483168b7f88f31db68ceea1419;p=Epimetheus%2F.git tweaks to structure --- diff --git a/developer/tool/release b/developer/tool/release new file mode 100755 index 0000000..e99629c --- /dev/null +++ b/developer/tool/release @@ -0,0 +1,291 @@ +#!/usr/bin/env -S python3 -B +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- + +import os, sys, shutil, stat, pwd, grp, glob, tempfile + +HELP = """usage: release {write|clean|ls|help|dry write} [DIR] + write [DIR] Writes released files into $REPO_HOME/release. If [DIR] is specified, only writes files found in scratchpad/DIR. + clean [DIR] Remove the contents of the release directories. If [DIR] is specified, clean only the contents of that release directory. + ls List release/ as an indented tree: PERMS OWNER NAME (root-level dotfiles printed first). + help Show this message. + dry write [DIR] + Preview what write would do without modifying the filesystem. +""" + +ENV_MUST_BE = "developer/tool/env" +DEFAULT_DIR_MODE = 0o700 # 077-congruent dirs + +def exit_with_status(msg, code=1): + print(f"release: {msg}", file=sys.stderr) + sys.exit(code) + +def assert_env(): + env = os.environ.get("ENV", "") + if env != ENV_MUST_BE: + hint = ( + "ENV is not 'developer/tool/env'.\n" + "Enter the project with: source ./env_developer\n" + "That script exports: ROLE=developer; ENV=$ROLE/tool/env" + ) + exit_with_status(f"bad environment: ENV='{env}'. {hint}") + +def repo_home(): + rh = os.environ.get("REPO_HOME") + if not rh: + exit_with_status("REPO_HOME not set (did you 'source ./env_developer'?)") + return rh + +def dpath(*parts): + return os.path.join(repo_home(), "developer", *parts) + +def rpath(*parts): + return os.path.join(repo_home(), "release", *parts) + +def dev_root(): + return dpath() + +def rel_root(): + return rpath() + +def _display_src(p_abs: str) -> str: + try: + if os.path.commonpath([dev_root()]) == os.path.commonpath([dev_root(), p_abs]): + return os.path.relpath(p_abs, dev_root()) + except Exception: + pass + return p_abs + +def _display_dst(p_abs: str) -> str: + try: + rel = os.path.relpath(p_abs, rel_root()) + rel = "" if rel == "." else rel + return "$REPO_HOME/release" + ("/" + rel if rel else "") + except Exception: + return p_abs + +def ensure_mode(path, mode): + try: os.chmod(path, mode) + except Exception: pass + +def ensure_dir(path, mode=DEFAULT_DIR_MODE, dry=False): + if dry: + if not os.path.isdir(path): + shown = _display_dst(path) if path.startswith(rel_root()) else ( + os.path.relpath(path, dev_root()) if path.startswith(dev_root()) else path + ) + print(f"(dry) mkdir -m {oct(mode)[2:]} '{shown}'") + return + os.makedirs(path, exist_ok=True) + ensure_mode(path, mode) + +def filemode(m): + try: return stat.filemode(m) + except Exception: return oct(m & 0o777) + +def owner_group(st): + try: return f"{pwd.getpwuid(st.st_uid).pw_name}:{grp.getgrgid(st.st_gid).gr_name}" + except Exception: return f"{st.st_uid}:{st.st_gid}" + +# ---------- LS (two-pass owner:group width) ---------- +def list_tree(root): + if not os.path.isdir(root): + return + entries = [] + def gather(path: str, depth: int, is_root: bool): + try: + it = list(os.scandir(path)) + except FileNotFoundError: + return + dirs = [e for e in it if e.is_dir(follow_symlinks=False)] + files = [e for e in it if not e.is_dir(follow_symlinks=False)] + dirs.sort(key=lambda e: e.name); files.sort(key=lambda e: e.name) + + if is_root: + for f in (e for e in files if e.name.startswith(".")): + st = os.lstat(f.path); entries.append((False, depth, filemode(st.st_mode), owner_group(st), f.name)) + for d in dirs: + st = os.lstat(d.path); entries.append((True, depth, filemode(st.st_mode), owner_group(st), d.name + "/")) + gather(d.path, depth + 1, False) + for f in (e for e in files if not e.name.startswith(".")): + st = os.lstat(f.path); entries.append((False, depth, filemode(st.st_mode), owner_group(st), f.name)) + else: + for d in dirs: + st = os.lstat(d.path); entries.append((True, depth, filemode(st.st_mode), owner_group(st), d.name + "/")) + gather(d.path, depth + 1, False) + for f in files: + st = os.lstat(f.path); entries.append((False, depth, filemode(st.st_mode), owner_group(st), f.name)) + gather(root, depth=1, is_root=True) + + ogw = 0 + for (_isdir, _depth, _perms, ownergrp, _name) in entries: + if len(ownergrp) > ogw: ogw = len(ownergrp) + + print("release/") + for (_isdir, depth, perms, ownergrp, name) in entries: + indent = " " * depth + print(f"{perms} {ownergrp:<{ogw}} {indent}{name}") +# ---------- end LS ---------- + +def iter_src_files(topdir, src_root): + base = os.path.join(src_root, topdir) if topdir else src_root + if not os.path.isdir(base): + return + yield + if topdir == "kmod": + for p in sorted(glob.glob(os.path.join(base, "*.ko"))): + yield (p, os.path.basename(p)) + else: + for root, dirs, files in os.walk(base): + dirs.sort(); files.sort() + for fn in files: + src = os.path.join(root, fn) + rel = os.path.relpath(src, base) + yield (src, rel) + +def _target_mode_from_source(src_abs: str) -> int: + """077 policy: files 0600; if source has owner-exec, make 0700.""" + try: + sm = stat.S_IMODE(os.stat(src_abs).st_mode) + except FileNotFoundError: + return 0o600 + return 0o700 if (sm & stat.S_IXUSR) else 0o600 + +def copy_one(src_abs, dst_abs, dry=False): + src_show = _display_src(src_abs) + dst_show = _display_dst(dst_abs) + parent = os.path.dirname(dst_abs) + os.makedirs(parent, exist_ok=True) + target_mode = _target_mode_from_source(src_abs) + + def _is_writable_dir(p): return os.access(p, os.W_OK) + flip_needed = not _is_writable_dir(parent) + restore_mode = None + parent_show = _display_dst(parent) + + if dry: + if flip_needed: + print(f"(dry) chmod u+w '{parent_show}'") + if os.path.exists(dst_abs): + print(f"(dry) unlink '{dst_show}'") + # show final mode we will set + print(f"(dry) install -m {oct(target_mode)[2:]} -D '{src_show}' '{dst_show}'") + if flip_needed: + print(f"(dry) chmod u-w '{parent_show}'") + return + + try: + if flip_needed: + try: + st_parent = os.stat(parent) + restore_mode = stat.S_IMODE(st_parent.st_mode) + os.chmod(parent, restore_mode | stat.S_IWUSR) + except PermissionError: + exit_with_status(f"cannot write: parent dir not writable and chmod failed on {parent_show}") + + # Atomic replace with enforced 077-compliant mode + fd, tmp_path = tempfile.mkstemp(prefix='.tmp.', dir=parent) + try: + with os.fdopen(fd, "wb") as tmpf, open(src_abs, "rb") as sf: + shutil.copyfileobj(sf, tmpf) + tmpf.flush() + os.chmod(tmp_path, target_mode) + os.replace(tmp_path, dst_abs) + finally: + try: + if os.path.exists(tmp_path): + os.unlink(tmp_path) + except Exception: + pass + finally: + if restore_mode is not None: + try: os.chmod(parent, restore_mode) + except Exception: pass + + print(f"+ install -m {oct(target_mode)[2:]} '{src_show}' '{dst_show}'") + +def write_one_dir(topdir, dry): + rel_root_dir = rpath() + src_root = dpath("scratchpad") + src_dir = os.path.join(src_root, topdir) + dst_dir = os.path.join(rel_root_dir, topdir) + + if not os.path.isdir(src_dir): + exit_with_status( + f"cannot write: expected '{_display_src(src_dir)}' to exist. " + f"Create scratchpad/{topdir} (Makefiles may need to populate it)." + ) + + ensure_dir(dst_dir, DEFAULT_DIR_MODE, dry=dry) + + wrote = False + for src_abs, rel in iter_src_files(topdir, src_root): + dst_abs = os.path.join(dst_dir, rel) + copy_one(src_abs, dst_abs, dry=dry) + wrote = True + if not wrote: + msg = "no matching artifacts found" + if topdir == "kmod": msg += " (looking for *.ko)" + print(f"(info) {msg} in {_display_src(src_dir)}") + +def cmd_write(dir_arg, dry=False): + assert_env() + ensure_dir(rpath(), DEFAULT_DIR_MODE, dry=dry) + + src_root = dpath("scratchpad") + if not os.path.isdir(src_root): + exit_with_status(f"cannot find developer scratchpad at '{_display_src(src_root)}'") + + if dir_arg: + write_one_dir(dir_arg, dry=dry) + else: + subs = sorted([e.name for e in os.scandir(src_root) if e.is_dir(follow_symlinks=False)]) + if not subs: + print(f"(info) nothing to release; no subdirectories found under {_display_src(src_root)}") + return + for td in subs: + write_one_dir(td, dry=dry) + +def _clean_contents(dir_path): + if not os.path.isdir(dir_path): return + for name in os.listdir(dir_path): + p = os.path.join(dir_path, name) + if os.path.isdir(p) and not os.path.islink(p): + shutil.rmtree(p, ignore_errors=True) + else: + try: os.unlink(p) + except FileNotFoundError: pass + +def cmd_clean(dir_arg): + assert_env() + rel_root_dir = rpath() + if not os.path.isdir(rel_root_dir): + return + if dir_arg: + _clean_contents(os.path.join(rel_root_dir, dir_arg)) + else: + for e in os.scandir(rel_root_dir): + if e.is_dir(follow_symlinks=False): + _clean_contents(e.path) + +def CLI(): + if len(sys.argv) < 2: + print(HELP); return + cmd, *args = sys.argv[1:] + if cmd == "write": + cmd_write(args[0] if args else None, dry=False) + elif cmd == "clean": + cmd_clean(args[0] if args else None) + elif cmd == "ls": + list_tree(rpath()) + elif cmd == "help": + print(HELP) + elif cmd == "dry": + if args and args[0] == "write": + cmd_write(args[1] if len(args) >= 2 else None, dry=True) + else: + print(HELP) + else: + print(HELP) + +if __name__ == "__main__": + CLI() diff --git a/shared/authored/env b/shared/authored/env index 35e91e4..e23af25 100644 --- a/shared/authored/env +++ b/shared/authored/env @@ -20,7 +20,7 @@ umask 0077 dirname "$script_afp" } -# assume this script is located $REPO_HOME/tools_shared/authored and work backwards +# assume this script is located $REPO_HOME/shared/authored and work backwards # to get $REPO_HOME, etc. REPO_HOME=$(dirname "$(dirname "$(script_adp)")") diff --git a/shared/authored/sys b/shared/authored/sys deleted file mode 100644 index e69de29..0000000 diff --git a/shared/made/version b/shared/made/version new file mode 120000 index 0000000..90de461 --- /dev/null +++ b/shared/made/version @@ -0,0 +1 @@ +../authored/version \ No newline at end of file diff --git a/shared/made/walk b/shared/made/walk deleted file mode 120000 index 1325c19..0000000 --- a/shared/made/walk +++ /dev/null @@ -1 +0,0 @@ -../authored/gitignore_treewalk.py \ No newline at end of file diff --git a/tool/Harmony_sync b/tool/Harmony_sync deleted file mode 120000 index 22ddb4e..0000000 --- a/tool/Harmony_sync +++ /dev/null @@ -1 +0,0 @@ -../shared/authored/Harmony_sync/CLI.py \ No newline at end of file diff --git a/tool/after_pull b/tool/after_pull deleted file mode 100755 index 946d9ad..0000000 --- a/tool/after_pull +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env -S python3 -B -# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- - -""" -set_project_permissions — normalize a freshly cloned project to Harmony policies. - -usage: - set_project_permissions [default] - set_project_permissions help | --help | -h - -notes: - • Must be run from the toolsmith environment (ENV=tool/env, ROLE=toolsmith). - • Starts at $REPO_HOME. - • Baseline is umask-077 congruence: - - directories → 0700 - - files → 0600, but preserve owner-exec (→ 0700 for executables) - applied to the entire repo, including release/, EXCEPT: - - release/kmod/*.ko → 0440 - • Skips .git/ and symlinks. -""" - -import os, sys, stat - -# Must match shared/authored/env policy: -DEFAULT_UMASK = 0o077 # reminder only; effective modes below implement 077 congruence. - -DIR_MODE_077 = 0o700 - -def die(msg, code=1): - print(f"set_project_permissions: {msg}", file=sys.stderr) - sys.exit(code) - -def require_toolsmith_env(): - env = os.environ.get("ENV", "") - role = os.environ.get("ROLE", "") - if env != "tool/env" or role != "toolsmith": - hint = ( - "This script should be run from the toolsmith environment.\n" - "Try: source ./env_toolsmith (then re-run: set_project_permissions default)" - ) - die(f"bad environment: ENV='{env}' ROLE='{role}'.\n{hint}") - -def repo_home(): - rh = os.environ.get("REPO_HOME") - if not rh: - die("REPO_HOME is not set (did you source shared/authored/env?)") - return os.path.realpath(rh) - -def show_path(p, rh): - return p.replace(rh, "$REPO_HOME", 1) if p.startswith(rh) else p - -def is_git_dir(path): - return os.path.basename(path.rstrip(os.sep)) == ".git" - -def file_target_mode_077_preserve_exec(current_mode: int) -> int: - # Base 0600, add owner exec if currently set; drop all group/other. - target = 0o600 - if current_mode & stat.S_IXUSR: - target |= stat.S_IXUSR - return target - -def set_mode_if_needed(path, target, rh): - try: - st = os.lstat(path) - except FileNotFoundError: - return 0 - cur = stat.S_IMODE(st.st_mode) - if cur == target: - return 0 - os.chmod(path, target) - print(f"+ chmod {oct(target)[2:]} '{show_path(path, rh)}'") - return 1 - -def apply_policy(rh): - changed = 0 - release_root = os.path.join(rh, "release") - for dirpath, dirnames, filenames in os.walk(rh, topdown=True, followlinks=False): - # prune .git - dirnames[:] = [d for d in dirnames if d != ".git"] - - # directories: 0700 everywhere (incl. release/) - changed += set_mode_if_needed(dirpath, DIR_MODE_077, rh) - - # files: 0600 (+owner exec) everywhere, except release/kmod/*.ko → 0440 - rel_from_repo = os.path.relpath(dirpath, rh) - under_release = rel_from_repo == "release" or rel_from_repo.startswith("release"+os.sep) - top_under_release = "" - if under_release: - rel_from_release = os.path.relpath(dirpath, release_root) - top_under_release = (rel_from_release.split(os.sep, 1)[0] if rel_from_release != "." else "") - - for fn in filenames: - p = os.path.join(dirpath, fn) - if os.path.islink(p): - continue - try: - st = os.lstat(p) - except FileNotFoundError: - continue - - if under_release and top_under_release == "kmod" and fn.endswith(".ko"): - target = 0o440 - else: - target = file_target_mode_077_preserve_exec(stat.S_IMODE(st.st_mode)) - - changed += set_mode_if_needed(p, target, rh) - return changed - -def cmd_default(): - require_toolsmith_env() - rh = repo_home() - total = apply_policy(rh) - print(f"changes: {total}") - -def main(): - if len(sys.argv) == 1 or sys.argv[1] in ("default",): - return cmd_default() - if sys.argv[1] in ("help", "--help", "-h"): - print(__doc__.strip()); return 0 - # unknown command → help - print(__doc__.strip()); return 1 - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tool/release b/tool/release deleted file mode 100755 index e99629c..0000000 --- a/tool/release +++ /dev/null @@ -1,291 +0,0 @@ -#!/usr/bin/env -S python3 -B -# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- - -import os, sys, shutil, stat, pwd, grp, glob, tempfile - -HELP = """usage: release {write|clean|ls|help|dry write} [DIR] - write [DIR] Writes released files into $REPO_HOME/release. If [DIR] is specified, only writes files found in scratchpad/DIR. - clean [DIR] Remove the contents of the release directories. If [DIR] is specified, clean only the contents of that release directory. - ls List release/ as an indented tree: PERMS OWNER NAME (root-level dotfiles printed first). - help Show this message. - dry write [DIR] - Preview what write would do without modifying the filesystem. -""" - -ENV_MUST_BE = "developer/tool/env" -DEFAULT_DIR_MODE = 0o700 # 077-congruent dirs - -def exit_with_status(msg, code=1): - print(f"release: {msg}", file=sys.stderr) - sys.exit(code) - -def assert_env(): - env = os.environ.get("ENV", "") - if env != ENV_MUST_BE: - hint = ( - "ENV is not 'developer/tool/env'.\n" - "Enter the project with: source ./env_developer\n" - "That script exports: ROLE=developer; ENV=$ROLE/tool/env" - ) - exit_with_status(f"bad environment: ENV='{env}'. {hint}") - -def repo_home(): - rh = os.environ.get("REPO_HOME") - if not rh: - exit_with_status("REPO_HOME not set (did you 'source ./env_developer'?)") - return rh - -def dpath(*parts): - return os.path.join(repo_home(), "developer", *parts) - -def rpath(*parts): - return os.path.join(repo_home(), "release", *parts) - -def dev_root(): - return dpath() - -def rel_root(): - return rpath() - -def _display_src(p_abs: str) -> str: - try: - if os.path.commonpath([dev_root()]) == os.path.commonpath([dev_root(), p_abs]): - return os.path.relpath(p_abs, dev_root()) - except Exception: - pass - return p_abs - -def _display_dst(p_abs: str) -> str: - try: - rel = os.path.relpath(p_abs, rel_root()) - rel = "" if rel == "." else rel - return "$REPO_HOME/release" + ("/" + rel if rel else "") - except Exception: - return p_abs - -def ensure_mode(path, mode): - try: os.chmod(path, mode) - except Exception: pass - -def ensure_dir(path, mode=DEFAULT_DIR_MODE, dry=False): - if dry: - if not os.path.isdir(path): - shown = _display_dst(path) if path.startswith(rel_root()) else ( - os.path.relpath(path, dev_root()) if path.startswith(dev_root()) else path - ) - print(f"(dry) mkdir -m {oct(mode)[2:]} '{shown}'") - return - os.makedirs(path, exist_ok=True) - ensure_mode(path, mode) - -def filemode(m): - try: return stat.filemode(m) - except Exception: return oct(m & 0o777) - -def owner_group(st): - try: return f"{pwd.getpwuid(st.st_uid).pw_name}:{grp.getgrgid(st.st_gid).gr_name}" - except Exception: return f"{st.st_uid}:{st.st_gid}" - -# ---------- LS (two-pass owner:group width) ---------- -def list_tree(root): - if not os.path.isdir(root): - return - entries = [] - def gather(path: str, depth: int, is_root: bool): - try: - it = list(os.scandir(path)) - except FileNotFoundError: - return - dirs = [e for e in it if e.is_dir(follow_symlinks=False)] - files = [e for e in it if not e.is_dir(follow_symlinks=False)] - dirs.sort(key=lambda e: e.name); files.sort(key=lambda e: e.name) - - if is_root: - for f in (e for e in files if e.name.startswith(".")): - st = os.lstat(f.path); entries.append((False, depth, filemode(st.st_mode), owner_group(st), f.name)) - for d in dirs: - st = os.lstat(d.path); entries.append((True, depth, filemode(st.st_mode), owner_group(st), d.name + "/")) - gather(d.path, depth + 1, False) - for f in (e for e in files if not e.name.startswith(".")): - st = os.lstat(f.path); entries.append((False, depth, filemode(st.st_mode), owner_group(st), f.name)) - else: - for d in dirs: - st = os.lstat(d.path); entries.append((True, depth, filemode(st.st_mode), owner_group(st), d.name + "/")) - gather(d.path, depth + 1, False) - for f in files: - st = os.lstat(f.path); entries.append((False, depth, filemode(st.st_mode), owner_group(st), f.name)) - gather(root, depth=1, is_root=True) - - ogw = 0 - for (_isdir, _depth, _perms, ownergrp, _name) in entries: - if len(ownergrp) > ogw: ogw = len(ownergrp) - - print("release/") - for (_isdir, depth, perms, ownergrp, name) in entries: - indent = " " * depth - print(f"{perms} {ownergrp:<{ogw}} {indent}{name}") -# ---------- end LS ---------- - -def iter_src_files(topdir, src_root): - base = os.path.join(src_root, topdir) if topdir else src_root - if not os.path.isdir(base): - return - yield - if topdir == "kmod": - for p in sorted(glob.glob(os.path.join(base, "*.ko"))): - yield (p, os.path.basename(p)) - else: - for root, dirs, files in os.walk(base): - dirs.sort(); files.sort() - for fn in files: - src = os.path.join(root, fn) - rel = os.path.relpath(src, base) - yield (src, rel) - -def _target_mode_from_source(src_abs: str) -> int: - """077 policy: files 0600; if source has owner-exec, make 0700.""" - try: - sm = stat.S_IMODE(os.stat(src_abs).st_mode) - except FileNotFoundError: - return 0o600 - return 0o700 if (sm & stat.S_IXUSR) else 0o600 - -def copy_one(src_abs, dst_abs, dry=False): - src_show = _display_src(src_abs) - dst_show = _display_dst(dst_abs) - parent = os.path.dirname(dst_abs) - os.makedirs(parent, exist_ok=True) - target_mode = _target_mode_from_source(src_abs) - - def _is_writable_dir(p): return os.access(p, os.W_OK) - flip_needed = not _is_writable_dir(parent) - restore_mode = None - parent_show = _display_dst(parent) - - if dry: - if flip_needed: - print(f"(dry) chmod u+w '{parent_show}'") - if os.path.exists(dst_abs): - print(f"(dry) unlink '{dst_show}'") - # show final mode we will set - print(f"(dry) install -m {oct(target_mode)[2:]} -D '{src_show}' '{dst_show}'") - if flip_needed: - print(f"(dry) chmod u-w '{parent_show}'") - return - - try: - if flip_needed: - try: - st_parent = os.stat(parent) - restore_mode = stat.S_IMODE(st_parent.st_mode) - os.chmod(parent, restore_mode | stat.S_IWUSR) - except PermissionError: - exit_with_status(f"cannot write: parent dir not writable and chmod failed on {parent_show}") - - # Atomic replace with enforced 077-compliant mode - fd, tmp_path = tempfile.mkstemp(prefix='.tmp.', dir=parent) - try: - with os.fdopen(fd, "wb") as tmpf, open(src_abs, "rb") as sf: - shutil.copyfileobj(sf, tmpf) - tmpf.flush() - os.chmod(tmp_path, target_mode) - os.replace(tmp_path, dst_abs) - finally: - try: - if os.path.exists(tmp_path): - os.unlink(tmp_path) - except Exception: - pass - finally: - if restore_mode is not None: - try: os.chmod(parent, restore_mode) - except Exception: pass - - print(f"+ install -m {oct(target_mode)[2:]} '{src_show}' '{dst_show}'") - -def write_one_dir(topdir, dry): - rel_root_dir = rpath() - src_root = dpath("scratchpad") - src_dir = os.path.join(src_root, topdir) - dst_dir = os.path.join(rel_root_dir, topdir) - - if not os.path.isdir(src_dir): - exit_with_status( - f"cannot write: expected '{_display_src(src_dir)}' to exist. " - f"Create scratchpad/{topdir} (Makefiles may need to populate it)." - ) - - ensure_dir(dst_dir, DEFAULT_DIR_MODE, dry=dry) - - wrote = False - for src_abs, rel in iter_src_files(topdir, src_root): - dst_abs = os.path.join(dst_dir, rel) - copy_one(src_abs, dst_abs, dry=dry) - wrote = True - if not wrote: - msg = "no matching artifacts found" - if topdir == "kmod": msg += " (looking for *.ko)" - print(f"(info) {msg} in {_display_src(src_dir)}") - -def cmd_write(dir_arg, dry=False): - assert_env() - ensure_dir(rpath(), DEFAULT_DIR_MODE, dry=dry) - - src_root = dpath("scratchpad") - if not os.path.isdir(src_root): - exit_with_status(f"cannot find developer scratchpad at '{_display_src(src_root)}'") - - if dir_arg: - write_one_dir(dir_arg, dry=dry) - else: - subs = sorted([e.name for e in os.scandir(src_root) if e.is_dir(follow_symlinks=False)]) - if not subs: - print(f"(info) nothing to release; no subdirectories found under {_display_src(src_root)}") - return - for td in subs: - write_one_dir(td, dry=dry) - -def _clean_contents(dir_path): - if not os.path.isdir(dir_path): return - for name in os.listdir(dir_path): - p = os.path.join(dir_path, name) - if os.path.isdir(p) and not os.path.islink(p): - shutil.rmtree(p, ignore_errors=True) - else: - try: os.unlink(p) - except FileNotFoundError: pass - -def cmd_clean(dir_arg): - assert_env() - rel_root_dir = rpath() - if not os.path.isdir(rel_root_dir): - return - if dir_arg: - _clean_contents(os.path.join(rel_root_dir, dir_arg)) - else: - for e in os.scandir(rel_root_dir): - if e.is_dir(follow_symlinks=False): - _clean_contents(e.path) - -def CLI(): - if len(sys.argv) < 2: - print(HELP); return - cmd, *args = sys.argv[1:] - if cmd == "write": - cmd_write(args[0] if args else None, dry=False) - elif cmd == "clean": - cmd_clean(args[0] if args else None) - elif cmd == "ls": - list_tree(rpath()) - elif cmd == "help": - print(HELP) - elif cmd == "dry": - if args and args[0] == "write": - cmd_write(args[1] if len(args) >= 2 else None, dry=True) - else: - print(HELP) - else: - print(HELP) - -if __name__ == "__main__": - CLI()