"""
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)
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())
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
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))
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):
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)
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:
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:
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()
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"
--- /dev/null
+#!/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())