more sophisticated release script
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Wed, 29 Oct 2025 16:10:24 +0000 (16:10 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Wed, 29 Oct 2025 16:10:24 +0000 (16:10 +0000)
developer/document/tests.sh [deleted file]
developer/tool/makefile_mod [deleted file]
developer/tool/release
developer/tool/release_clean [deleted file]
release/machine/hello [new file with mode: 0755]
release/python3/.githolder [deleted file]
release/shell/.githolder [deleted file]

diff --git a/developer/document/tests.sh b/developer/document/tests.sh
deleted file mode 100644 (file)
index 6892145..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-2025-05-19T09:56:17Z[Harmony]
-Thomas-developer@StanleyPark§/home/Thomas/subu_data/developer/Harmony/developer/python§
-> python test_isqrt.py 500000 20000
-....................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
-✅ Passed: 500000
-❌ Failed: 0
-
-
diff --git a/developer/tool/makefile_mod b/developer/tool/makefile_mod
deleted file mode 100644 (file)
index 50485ca..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-# tool/makefile
-
-RT_project_share:=$(REPO_HOME)/tool_shared/third_party/RT-project-share/release
-include $(RT_project_share)/make/environment_RT_0
-
-KBUILD_SRCDIR_List := $(SRCDIR_List)
-KBUILD_SRC_List := $(foreach dir, $(KBUILD_SRCDIR_List), $(wildcard $(dir)/*.mod.c))
-KBUILD_BASE_List := $(sort $(patsubst %.mod.c, %, $(notdir $(KBUILD_SRC_List))))
-
-DEPFILE := $(TMPDIR)/makefile-cc.deps
-
--include $(DEPFILE)
-include $(RT_project_share)/make/targets_developer
-include $(RT_project_share)/make/targets_kernel
-
-# ----------------------------------------------------------------------
-# --- PUBLIC TARGET ORCHESTRATION (Must be last to win precedence) ---
-# ----------------------------------------------------------------------
-
-# 1. DEFAULT TARGET: Make the 'all' target the default for running 'make'.
-#    This must be defined last to override the 'all: usage' from the included targets files.
-.PHONY: all
-all: kernel_module
-
-# 2. CLEAN TARGET: Orchestrate the cleanup process.
-.PHONY: clean
-clean: clean_developer clean_kernel
index 63f0378..e29cb43 100755 (executable)
-#!/usr/bin/env bash
-script_afp=$(realpath "${BASH_SOURCE[0]}")
-
-# This is a bespoke script for copying files into ../release.
-# Files in ../release can be used by the tester role.
-# On a release branch, files in the ../release constitute the public release.
-
-# conventional preliminaries
-
-  env_must_be="developer/tool/env"
-  if [ "$ENV" != "$env_must_be" ]; then
-    echo "$(script_fp):: error: must be run in the $env_must_be environment"
-    exit 1
-  fi
-
-  cd "$REPO_HOME"/developer || exit 1
-
-#set -e
-#set -x
-
-cd "$REPO_HOME/developer"
-
-release_dir="$REPO_HOME/release"
-ko_dir="scratchpad/kmod"
-machine_executable_list="scratchpad/hello"
-
-# Enable nullglob so an empty match yields an empty list (not the literal pattern)
-shopt -s nullglob
-
-# zero or more machine executables
-for f in $machine_executable_list; do
-  if [[ -x "$f" ]]; then
-    echo "+ install -m 0500 '$f' '$release_dir/machine/'"
-    install -m 0550 "$f" "$release_dir/machine/"
-  else
-    echo "(info) did not find '$f'"
-  fi
-done
-
-# zero or more Kernel modules
-# release/kmod created 750 so developer can write to it, group can read it
-ko_list=("$ko_dir"/*.ko)
-if (( ${#ko_list[@]} )); then
-  if [[ ! -d "$release_dir/kmod" ]]; then
-    echo "+ install -d '$release_dir/kmod'"
-    install -m 750 -d "$release_dir/kmod"
-  fi
-  for f in "${ko_list[@]}"; do
-    echo "+ install -m 0440 '$f' '$release_dir/kmod/'"
-    install -m 440 "$f" "$release_dir/kmod/"
-  done
-else
-  echo "(info) no kmod artifacts found in $ko_dir; skipping kmod release"
-fi
-
-echo "$script_fn done."
+#!/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 = 0o750
+PERM_BY_DIR = {
+  "kmod":    0o440,
+  "machine": 0o550,
+  "python3": 0o550,
+  "shell":   0o550,
+}
+
+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:
+  # Developer paths shown relative to $REPO_HOME/developer
+  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:
+  # Release paths shown as literal '$REPO_HOME/release/<rel>'
+  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 with two-pass column width for owner:group ----------
+def list_tree(root):
+  if not os.path.isdir(root):
+    return
+
+  # gather entries in display order, record owner:group widths
+  entries = []  # list of (is_dir, depth, perms, ownergrp, name)
+  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:
+      # root-level: dotfiles first
+      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:
+      # subdirs: dirs then files (dotfiles naturally sort first)
+      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)
+
+  # compute max width for owner:group column
+  ogw = 0
+  for (_isdir, _depth, _perms, ownergrp, _name) in entries:
+    if len(ownergrp) > ogw:
+      ogw = len(ownergrp)
+
+  # print
+  print("release/")
+  for (isdir, depth, perms, ownergrp, name) in entries:
+    indent = "  " * depth
+    # perms first, owner:group padded next, then name with tree indent
+    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(topdir):
+  return PERM_BY_DIR.get(topdir, 0o440)
+
+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
+
+  # Replace even if dst exists and is read-only: write temp then atomic replace.
+  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 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
+  mode = target_mode(topdir)
+  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, mode, 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/developer/tool/release_clean b/developer/tool/release_clean
deleted file mode 100755 (executable)
index fc09a13..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/usr/bin/env bash
-script_afp=$(realpath "${BASH_SOURCE[0]}")
-
-# before running this, make library is built and is in the scratchpad directory
-
-# input guards
-
-  env_must_be="developer/tool/env"
-  if [ "$ENV" != "$env_must_be" ]; then
-    echo "$(script_fp):: error: must be run in the $env_must_be environment"
-    exit 1
-  fi
-
-set -e
-set -x
-
-  cd "$REPO_HOME"/developer || exit 1
-
-  release_dir=$(release_dir)
-
-  if [ ! -d ${release_dir} ]; then
-    echo "$(script_fp):: no release directory: " ${release_dir}
-    exit 1
-  fi
-
-  rm_na -rf ${release_dir}/*
-
-set +x
-echo "$(script_fn) done."
-
diff --git a/release/machine/hello b/release/machine/hello
new file mode 100755 (executable)
index 0000000..9f26c0f
Binary files /dev/null and b/release/machine/hello differ
diff --git a/release/python3/.githolder b/release/python3/.githolder
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/release/shell/.githolder b/release/shell/.githolder
deleted file mode 100644 (file)
index e69de29..0000000