updated Harmony release
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Wed, 29 Oct 2025 19:16:39 +0000 (19:16 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Wed, 29 Oct 2025 19:16:39 +0000 (19:16 +0000)
developer/tool/release
tool/set_project_permissions [new file with mode: 0755]

index ef04c15..e99629c 100755 (executable)
@@ -13,13 +13,7 @@ HELP = """usage: release {write|clean|ls|help|dry write} [DIR]
 """
 
 ENV_MUST_BE = "developer/tool/env"
-DEFAULT_DIR_MODE = 0o750
-PERM_BY_DIR = {
-  "kmod":    0o440,
-  "machine": 0o550,
-  "python3": 0o550,
-  "shell":   0o550,
-}
+DEFAULT_DIR_MODE = 0o700  # 077-congruent dirs
 
 def exit_with_status(msg, code=1):
   print(f"release: {msg}", file=sys.stderr)
@@ -54,7 +48,6 @@ 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())
@@ -63,7 +56,6 @@ def _display_src(p_abs: str) -> str:
   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
@@ -94,13 +86,11 @@ 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 ----------
+# ---------- LS (two-pass owner:group width) ----------
 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)
+  entries = []
   def gather(path: str, depth: int, is_root: bool):
     try:
       it = list(os.scandir(path))
@@ -108,46 +98,32 @@ def list_tree(root):
       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)
+    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))
+        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 + "/"))
+        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))
+        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 + "/"))
+        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))
-
+        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)
+    if len(ownergrp) > ogw: ogw = len(ownergrp)
 
-  # print
   print("release/")
-  for (isdir, depth, perms, ownergrp, name) in entries:
+  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):
@@ -166,20 +142,22 @@ def iter_src_files(topdir, src_root):
         rel = os.path.relpath(src, base)
         yield (src, rel)
 
-def target_mode(topdir):
-  return PERM_BY_DIR.get(topdir, 0o440)
+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, mode, dry=False):
+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)
 
-  # helper: check parent-dir writability
-  def _is_writable_dir(p):
-    return os.access(p, os.W_OK)
-
-  # determine if we must flip u+w on parent
+  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)
@@ -189,12 +167,12 @@ def copy_one(src_abs, dst_abs, mode, dry=False):
       print(f"(dry) chmod u+w '{parent_show}'")
     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}'")
+    # 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
 
-  # real path: flip write bit if needed
   try:
     if flip_needed:
       try:
@@ -204,13 +182,13 @@ def copy_one(src_abs, dst_abs, mode, dry=False):
       except PermissionError:
         exit_with_status(f"cannot write: parent dir not writable and chmod failed on {parent_show}")
 
-    # Replace even if dst exists and is read-only: temp then atomic replace.
+    # 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, mode)
+      os.chmod(tmp_path, target_mode)
       os.replace(tmp_path, dst_abs)
     finally:
       try:
@@ -219,14 +197,11 @@ def copy_one(src_abs, dst_abs, mode, dry=False):
       except Exception:
         pass
   finally:
-    # restore parent dir mode if we flipped it
     if restore_mode is not None:
-      try:
-        os.chmod(parent, restore_mode)
-      except Exception:
-        pass
+      try: os.chmod(parent, restore_mode)
+      except Exception: pass
 
-  print(f"+ install -m {oct(mode)[2:]} '{src_show}' '{dst_show}'")
+  print(f"+ install -m {oct(target_mode)[2:]} '{src_show}' '{dst_show}'")
 
 def write_one_dir(topdir, dry):
   rel_root_dir = rpath()
@@ -243,10 +218,9 @@ def write_one_dir(topdir, dry):
   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)
+    copy_one(src_abs, dst_abs, dry=dry)
     wrote = True
   if not wrote:
     msg = "no matching artifacts found"
diff --git a/tool/set_project_permissions b/tool/set_project_permissions
new file mode 100755 (executable)
index 0000000..834802a
--- /dev/null
@@ -0,0 +1,101 @@
+#!/usr/bin/env -S python3 -B
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+"""
+set_project_permissions — normalize a project to umask-077 congruence.
+
+usage:
+  set_project_permissions <command>
+
+command::
+  ε | default | help | --help | -h
+
+notes:
+  • Must be run from the toolsmith environment (ENV=tool/env, ROLE=toolsmith).
+  • Starts at $REPO_HOME.
+  • Directories → 0700
+  • Files → 0600, preserving owner-exec (→ 0700)
+  • Skips .git/ and symlinks.
+"""
+
+import os, sys, stat
+
+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":
+    die("bad environment: requires ENV=tool/env and ROLE=toolsmith\n"
+        "hint: source ./env_toolsmith, then run: set_project_permissions default")
+
+def repo_home():
+  rh = os.environ.get("REPO_HOME")
+  if not rh:
+    die("REPO_HOME not set (did you source tool_shared/bespoke/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 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 file_target_mode_077_preserve_exec(cur: int) -> int:
+  return 0o700 if (cur & stat.S_IXUSR) else 0o600
+
+def apply_policy(rh):
+  changed = 0
+  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
+    changed += set_mode_if_needed(dirpath, DIR_MODE_077, rh)
+    # files → 0600 (or 0700 if owner-exec already set)
+    for fn in filenames:
+      p = os.path.join(dirpath, fn)
+      if os.path.islink(p):
+        continue
+      try:
+        st = os.lstat(p)
+      except FileNotFoundError:
+        continue
+      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():
+  # No args => show usage (do NOT run)
+  if len(sys.argv) == 1:
+    print(__doc__.strip()); return 1
+
+  cmd = sys.argv[1]
+  if cmd in ("help", "--help", "-h"):
+    print(__doc__.strip()); return 0
+  if cmd in ("default", "e"):
+    cmd_default(); return 0
+
+  # Unknown command => help
+  print(__doc__.strip()); return 1
+
+if __name__ == "__main__":
+  sys.exit(main())