From 98d8777f62c06b75d31b54caa9cf4c2a166728e2 Mon Sep 17 00:00:00 2001 From: Thomas Walker Lynch Date: Thu, 25 Jun 2026 15:15:13 +0000 Subject: [PATCH] gitignore tweak, fix build/promote bug, removes make level --- .gitignore | 9 +- developer/tool/build_component/copy | 76 ++++++-- developer/tool/promote | 257 ++++++++++------------------ 3 files changed, 154 insertions(+), 188 deletions(-) diff --git a/.gitignore b/.gitignore index 02e2a00..670a22b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,14 @@ -# Protect against accidental local .gitignore deletion + +# no archive or temporary files +*.tar +*.tgz +*.zip +*. +temp.* **/scratchpad/* !**/scratchpad/.gitignore + # Python __pycache__/ *.py[cod] diff --git a/developer/tool/build_component/copy b/developer/tool/build_component/copy index f4bbd5a..2939ef3 100755 --- a/developer/tool/build_component/copy +++ b/developer/tool/build_component/copy @@ -1,17 +1,59 @@ -#!/usr/bin/env bash -# developer/tool/copy - -set -euo pipefail -CMD="${1:-all}" - -if [ "$CMD" = "clean" ]; then - rm -rf "scratchpad/made/$NAMESPACE" - echo "copy: Cleaned scratchpad/made/$NAMESPACE" -elif [ "$CMD" = "all" ]; then - mkdir -p "scratchpad/made" - cp -au "authored/$NAMESPACE_DIR" "scratchpad/made/$NAMESPACE" - echo "copy: Staged $NAMESPACE_DIR -> scratchpad/made/$NAMESPACE" -else - echo "copy: Unrecognized command '$CMD'" - exit 1 -fi +#!/usr/bin/env -S python3 -B +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- +# developer/tool/build_component/copy + +import sys +import os +import shutil + +sys.path.insert(0, os.path.join(os.path.abspath("tool"), "build_component")) +import sync_engine + +HELP = """usage: copy {all|clean|help} + all Stages files from authored to scratchpad. + clean Removes the specified namespace from scratchpad. + help Shows this message. +""" + +def cmd_all(): + namespace = os.environ.get("NAMESPACE") + namespace_dir = os.environ.get("NAMESPACE_DIR") + + if not namespace or not namespace_dir: + print("copy: error: NAMESPACE and NAMESPACE_DIR environment variables must be set.", file=sys.stderr) + sys.exit(1) + + src_root = os.path.join("authored", namespace_dir) + dst_root = os.path.join("scratchpad", namespace) + + sync_engine.execute_sync(src_root, dst_root, prefix="copy", apply_modes=False) + +def cmd_clean(): + namespace = os.environ.get("NAMESPACE") + if not namespace: + print("copy: error: NAMESPACE environment variable must be set.", file=sys.stderr) + sys.exit(1) + + dst_root = os.path.join("scratchpad", namespace) + if os.path.exists(dst_root): + shutil.rmtree(dst_root) + print(f"copy: Cleaned {dst_root}") + +def CLI(): + if len(sys.argv) < 2: + cmd = "all" + else: + cmd = sys.argv[1] + + if cmd == "all": + cmd_all() + elif cmd == "clean": + cmd_clean() + elif cmd in ["help", "-h", "--help"]: + print(HELP) + else: + print(f"copy: Unrecognized command '{cmd}'", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + CLI() diff --git a/developer/tool/promote b/developer/tool/promote index 42015a2..fa97a2c 100755 --- a/developer/tool/promote +++ b/developer/tool/promote @@ -1,28 +1,37 @@ #!/usr/bin/env -S python3 -B # -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- +# developer/tool/promote -import os ,sys ,shutil ,stat ,pwd ,grp ,glob ,tempfile ,filecmp +import os +import sys +import shutil +import stat +import pwd +import grp +import filecmp + +sys.path.insert(0, os.path.join(os.path.abspath("tool"), "build_component")) +import sync_engine HELP = """usage: promote {write|clean|ls|diff|help|dry write} - write Writes promoted files from scratchpad/made into consumer. Only updates newer files. + write Writes promoted files from scratchpad into consumer. Only updates newer files. clean Remove all contents of the consumer directory (preserves .gitignore). ls List consumer as an indented tree: PERMS OWNER NAME. - diff List missing, orphaned, or out-of-sync files between scratchpad and consumer. + diff List missing, orphaned, or out of sync files between scratchpad and consumer. help Show this message. dry write Preview what write would do without modifying the filesystem. """ SETUP_MUST_BE = "developer/tool/setup" DEFAULT_DIR_MODE = 0o700 -DEFAULT_FILE_MODE = 0o400 -def exit_with_status(msg ,code=1): - print(f"release: {msg}" ,file=sys.stderr) +def exit_with_status(msg, code=1): + print(f"promote: {msg}", file=sys.stderr) sys.exit(code) def assert_setup(): - setup_val = os.environ.get("SETUP" ,"") - if( setup_val != SETUP_MUST_BE ): + setup_val = os.environ.get("SETUP", "") + if setup_val != SETUP_MUST_BE: hint = ( "SETUP is not 'developer/tool/setup'.\n" "Enter the project with: . setup developer\n" @@ -32,63 +41,25 @@ def assert_setup(): def repo_home(): rh = os.environ.get("REPO_HOME") - if( not rh ): + if not rh: exit_with_status("REPO_HOME not set") return rh def dpath(*parts): - return os.path.join( - repo_home() - ,"developer" - ,*parts - ) + return os.path.join(repo_home(), "developer", *parts) def cpath(*parts): - return os.path.join( - repo_home() - ,"consumer" - ,*parts - ) + return os.path.join(repo_home(), "consumer", *parts) -def dev_root(): - return dpath() - -def consumer_root(): - return cpath() - -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 - ,consumer_root() - ) - rel = "" if rel == "." else rel - return "$REPO_HOME/consumer" + ("/" + rel if rel else "") - except Exception: - return p_abs - -def ensure_mode(path_str ,mode): - try: os.chmod(path_str ,mode) +def ensure_mode(path_str, mode): + try: os.chmod(path_str, mode) except Exception: pass -def ensure_dir(path_str ,mode=DEFAULT_DIR_MODE ,dry=False): - if( dry ): - if( not os.path.isdir(path_str) ): - shown = _display_dst(path_str) if path_str.startswith(consumer_root()) else ( - os.path.relpath(path_str ,dev_root()) if path_str.startswith(dev_root()) else path_str - ) - print(f"(dry) mkdir -m {oct(mode)[2:]} '{shown}'") +def ensure_dir(path_str, mode=DEFAULT_DIR_MODE, dry=False): + if dry: return - os.makedirs(path_str ,exist_ok=True) - ensure_mode(path_str ,mode) + os.makedirs(path_str, exist_ok=True) + ensure_mode(path_str, mode) def filemode(m): try: return stat.filemode(m) @@ -99,11 +70,11 @@ def owner_group(st): except Exception: return f"{st.st_uid}:{st.st_gid}" def list_tree(root_dp): - if( not os.path.isdir(root_dp) ): + if not os.path.isdir(root_dp): return - + entries = [] - def gather(path_str ,depth ,is_root): + def gather(path_str, depth, is_root): try: it = list(os.scandir(path_str)) except FileNotFoundError: @@ -113,127 +84,75 @@ def list_tree(root_dp): dirs.sort(key=lambda TM_e: TM_e.name) files.sort(key=lambda TM_e: TM_e.name) - if( is_root ): - for TM_f in ( TM_e for TM_e in files if TM_e.name.startswith(".") ): + if is_root: + for TM_f in (TM_e for TM_e in files if TM_e.name.startswith(".")): st = os.lstat(TM_f.path) - entries.append((False ,depth ,filemode(st.st_mode) ,owner_group(st) ,TM_f.name)) + entries.append((False, depth, filemode(st.st_mode), owner_group(st), TM_f.name)) for TM_d in dirs: st = os.lstat(TM_d.path) - entries.append((True ,depth ,filemode(st.st_mode) ,owner_group(st) ,TM_d.name + "/")) - gather(TM_d.path ,depth + 1 ,False) - for TM_f in ( TM_e for TM_e in files if not TM_e.name.startswith(".") ): + entries.append((True, depth, filemode(st.st_mode), owner_group(st), TM_d.name + "/")) + gather(TM_d.path, depth + 1, False) + for TM_f in (TM_e for TM_e in files if not TM_e.name.startswith(".")): st = os.lstat(TM_f.path) - entries.append((False ,depth ,filemode(st.st_mode) ,owner_group(st) ,TM_f.name)) + entries.append((False, depth, filemode(st.st_mode), owner_group(st), TM_f.name)) else: for TM_d in dirs: st = os.lstat(TM_d.path) - entries.append((True ,depth ,filemode(st.st_mode) ,owner_group(st) ,TM_d.name + "/")) - gather(TM_d.path ,depth + 1 ,False) + entries.append((True, depth, filemode(st.st_mode), owner_group(st), TM_d.name + "/")) + gather(TM_d.path, depth + 1, False) for TM_f in files: st = os.lstat(TM_f.path) - entries.append((False ,depth ,filemode(st.st_mode) ,owner_group(st) ,TM_f.name)) + entries.append((False, depth, filemode(st.st_mode), owner_group(st), TM_f.name)) - gather(root_dp ,1 ,True) + gather(root_dp, 1, True) ogw = 0 - for TM_isdir ,TM_depth ,TM_perms ,TM_ownergrp ,TM_name in entries: - if( len(TM_ownergrp) > ogw ): + for TM_isdir, TM_depth, TM_perms, TM_ownergrp, TM_name in entries: + if len(TM_ownergrp) > ogw: ogw = len(TM_ownergrp) print("consumer/") - for TM_isdir ,TM_depth ,TM_perms ,TM_ownergrp ,TM_name in entries: + for TM_isdir, TM_depth, TM_perms, TM_ownergrp, TM_name in entries: indent = " " * TM_depth print(f"{TM_perms} {TM_ownergrp:<{ogw}} {indent}{TM_name}") -def copy_one(src_abs ,dst_abs ,mode ,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) - - if( dry ): - if( os.path.exists(dst_abs) ): - print(f"(dry) unlink '{dst_show}'") - print(f"(dry) install -m {oct(mode)[2:]} -D '{src_show}' '{dst_show}'") - return - - 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 ,mode) - os.replace(tmp_path ,dst_abs) - finally: - try: - if( os.path.exists(tmp_path) ): - os.unlink(tmp_path) - except Exception: - pass - - print(f"+ install -m {oct(mode)[2:]} '{src_show}' '{dst_show}'") - def cmd_write(dry=False): assert_setup() - ensure_dir(cpath() ,DEFAULT_DIR_MODE ,dry=dry) - - src_root = dpath( - "scratchpad" - ,"made" + dst_root = cpath() + ensure_dir(dst_root, DEFAULT_DIR_MODE, dry=dry) + src_root = dpath("scratchpad") + + sync_engine.execute_sync( + src_root, + dst_root, + prefix="promote", + dry=dry, + apply_modes=True, + ignore_list=[".gitignore"] ) - if( not os.path.isdir(src_root) ): - exit_with_status(f"cannot find developer scratchpad made at '{_display_src(src_root)}'") - - wrote = False - for TM_root ,TM_dirs ,TM_files in os.walk(src_root): - TM_dirs.sort() - TM_files.sort() - for TM_fn in TM_files: - src_abs = os.path.join(TM_root ,TM_fn) - rel = os.path.relpath(src_abs ,src_root) - dst_abs = os.path.join(cpath() ,rel) - - if( os.path.exists(dst_abs) ): - # Use content comparison instead of timestamps - if( filecmp.cmp(src_abs ,dst_abs ,shallow=False) ): - continue - - st = os.stat(src_abs) - is_exec = st.st_mode & stat.S_IXUSR - mode = 0o500 if is_exec else 0o400 - - copy_one( - src_abs - ,dst_abs - ,mode - ,dry=dry - ) - wrote = True - - if( not wrote ): - print(f"(info) nothing new to promote from {_display_src(src_root)}") def cmd_diff(): assert_setup() dst_root = cpath() - src_root = dpath( - "scratchpad" - ,"made" - ) + src_root = dpath("scratchpad") src_files = set() - if( os.path.isdir(src_root) ): - for TM_root ,TM_dirs ,TM_files in os.walk(src_root): + if os.path.isdir(src_root): + for TM_root, TM_dirs, TM_files in os.walk(src_root): for TM_fn in TM_files: - src_files.add(os.path.relpath(os.path.join(TM_root ,TM_fn) ,src_root)) + if TM_fn == ".gitignore": + continue + src_files.add(os.path.relpath(os.path.join(TM_root, TM_fn), src_root)) dst_files = set() - if( os.path.isdir(dst_root) ): - for TM_root ,TM_dirs ,TM_files in os.walk(dst_root): + if os.path.isdir(dst_root): + for TM_root, TM_dirs, TM_files in os.walk(dst_root): for TM_fn in TM_files: - dst_files.add(os.path.relpath(os.path.join(TM_root ,TM_fn) ,dst_root)) + if TM_fn == ".gitignore": + continue + dst_files.add(os.path.relpath(os.path.join(TM_root, TM_fn), dst_root)) - if( not src_files and not dst_files ): + if not src_files and not dst_files: print("No differences found. Both directories are empty or missing.") return @@ -241,66 +160,64 @@ def cmd_diff(): found_diff = False for TM_rel in all_files: - if( TM_rel not in dst_files ): + if TM_rel not in dst_files: print(f"Pending promotion (missing in consumer): {TM_rel}") found_diff = True - elif( TM_rel not in src_files ): + elif TM_rel not in src_files: print(f"Orphaned in consumer (missing in scratchpad): {TM_rel}") found_diff = True else: - src_abs = os.path.join(src_root ,TM_rel) - dst_abs = os.path.join(dst_root ,TM_rel) + src_abs = os.path.join(src_root, TM_rel) + dst_abs = os.path.join(dst_root, TM_rel) - # Compare contents first - if( not filecmp.cmp(src_abs ,dst_abs ,shallow=False) ): + if not filecmp.cmp(src_abs, dst_abs, shallow=False): src_mtime = os.stat(src_abs).st_mtime dst_mtime = os.stat(dst_abs).st_mtime - # If they differ, check timestamps to infer direction - if( src_mtime > dst_mtime ): + if src_mtime > dst_mtime: print(f"Pending update (contents differ, newer in scratchpad): {TM_rel}") else: print(f"Contents differ (locally modified in consumer): {TM_rel}") found_diff = True - if( not found_diff ): - print("No differences found. Consumer matches developer scratchpad made.") + if not found_diff: + print("No differences found. Consumer matches developer scratchpad.") def cmd_clean(): assert_setup() consumer_root_dir = cpath() - if( not os.path.isdir(consumer_root_dir) ): + if not os.path.isdir(consumer_root_dir): return for TM_name in os.listdir(consumer_root_dir): - if( TM_name == ".gitignore" ): + if TM_name == ".gitignore": continue - p = os.path.join(consumer_root_dir ,TM_name) - if( os.path.isdir(p) and not os.path.islink(p) ): - shutil.rmtree(p ,ignore_errors=True) + p = os.path.join(consumer_root_dir, TM_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 CLI(): - if( len(sys.argv) < 2 ): + if len(sys.argv) < 2: print(HELP) return cmd = sys.argv[1] args = sys.argv[2:] - if( cmd == "write" ): + if cmd == "write": cmd_write(dry=False) - elif( cmd == "clean" ): + elif cmd == "clean": cmd_clean() - elif( cmd == "ls" ): + elif cmd == "ls": list_tree(cpath()) - elif( cmd == "diff" ): + elif cmd == "diff": cmd_diff() - elif( cmd == "help" ): + elif cmd == "help": print(HELP) - elif( cmd == "dry" ): - if( args and args[0] == "write" ): + elif cmd == "dry": + if args and args[0] == "write": cmd_write(dry=True) else: print(HELP) -- 2.20.1