backing off of read only release dir due to complexity
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Wed, 29 Oct 2025 19:17:14 +0000 (19:17 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Wed, 29 Oct 2025 19:17:14 +0000 (19:17 +0000)
env_developer
env_toolsmith
tool/.githolder [deleted file]
tool/env [new file with mode: 0644]
tool/release [new file with mode: 0755]
tool/set_project_permissions [new file with mode: 0755]

index 892df91..59993f6 100644 (file)
@@ -1,4 +1,6 @@
 #!/usr/bin/env bash
+# env_developer — enter the project developer environment
+# (must be sourced)
 
 script_afp=$(realpath "${BASH_SOURCE[0]}")
 if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
index b121a82..2e27571 100644 (file)
@@ -1,11 +1,45 @@
 #!/usr/bin/env bash
+# env_toolsmith — enter the project toolsmith environment
+# (must be sourced)
+
 script_afp=$(realpath "${BASH_SOURCE[0]}")
 if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
   echo "$script_afp:: This script must be sourced, not executed."
   exit 1
 fi
 
-export ROLE=toolsmith
-source tool_shared/bespoke/env
-export ENV=$ROLE
+# enter project environment
+#
+  source tool_shared/bespoke/env
+
+# setup tools
+# initially these will not exist, as the toolsmith installs them
+#
+  export PYTHON_HOME="$REPO_HOME/tool_shared/third_party/python"
+  if [[ ":$PATH:" != *":$PYTHON_HOME/bin:"* ]]; then
+    export PATH="$PYTHON_HOME/bin:$PATH"
+  fi
+
+  RT_gcc="$REPO_HOME/tool_shared/third_party/RT_gcc/release"
+  if [[ ":$PATH:" != *":$RT_gcc:"* ]]; then
+    export PATH="$RT_gcc:$PATH"
+  fi
+
+# enter the role environment
+#
+  export ROLE=toolsmith
+
+  TOOL_DIR="$REPO_HOME/tool"
+  if [[ ":$PATH:" != *":$TOOL_DIR:"* ]]; then
+    export PATH="$TOOL_DIR:$PATH"
+  fi
+
+  export ENV="tool/env"
 
+  cd "$REPO_HOME"
+  if [[ -f "tool/env" ]]; then
+    source "tool/env"
+    echo "in environment: $ENV"
+  else
+    echo "not found: $ENV"
+  fi
diff --git a/tool/.githolder b/tool/.githolder
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/tool/env b/tool/env
new file mode 100644 (file)
index 0000000..0b993ad
--- /dev/null
+++ b/tool/env
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+script_afp=$(realpath "${BASH_SOURCE[0]}")
+
diff --git a/tool/release b/tool/release
new file mode 100755 (executable)
index 0000000..e99629c
--- /dev/null
@@ -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/tool/set_project_permissions b/tool/set_project_permissions
new file mode 100755 (executable)
index 0000000..3511c89
--- /dev/null
@@ -0,0 +1,124 @@
+#!/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 tool_shared/bespoke/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 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 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())