fix: verbose first-run debug output, root uses system dirs, non-root uses ~/.local

parent 23a9f726
...@@ -29,12 +29,22 @@ import shutil ...@@ -29,12 +29,22 @@ import shutil
from pathlib import Path from pathlib import Path
# Files and directories that must be present in the share directory for the # Files and directories the share directory must contain for the server to start.
# server to start correctly.
_REQUIRED_FILES = ['aisbf.sh', 'main.py', 'requirements.txt'] _REQUIRED_FILES = ['aisbf.sh', 'main.py', 'requirements.txt']
_REQUIRED_DIRS = ['templates', 'static', 'config'] _REQUIRED_DIRS = ['templates', 'static', 'config']
def _is_root():
return os.getuid() == 0
def _default_share_dest():
"""System dir for root, user dir for everyone else."""
if _is_root():
return Path('/usr/local/share/aisbf')
return Path.home() / '.local' / 'share' / 'aisbf'
def _share_dir_candidates(): def _share_dir_candidates():
"""Return candidate share directories in priority order, deduplicated.""" """Return candidate share directories in priority order, deduplicated."""
seen = set() seen = set()
...@@ -46,9 +56,12 @@ def _share_dir_candidates(): ...@@ -46,9 +56,12 @@ def _share_dir_candidates():
seen.add(key) seen.add(key)
result.append(Path(p)) result.append(Path(p))
# sysconfig paths for the running Python interpreter — covers venvs, # sysconfig paths for the running interpreter — covers venvs, system
# system installs, and user installs regardless of pip's install scheme. # installs, and all user-install schemes regardless of how pip was invoked.
add(Path(sysconfig.get_path('data')) / 'share' / 'aisbf') try:
add(Path(sysconfig.get_path('data')) / 'share' / 'aisbf')
except Exception:
pass
for scheme in ('posix_user', 'posix_prefix', 'posix_home'): for scheme in ('posix_user', 'posix_prefix', 'posix_home'):
try: try:
...@@ -56,7 +69,7 @@ def _share_dir_candidates(): ...@@ -56,7 +69,7 @@ def _share_dir_candidates():
except Exception: except Exception:
pass pass
# Legacy hardcoded fallbacks # Hardcoded legacy fallbacks
add(Path('/usr/local/share/aisbf')) add(Path('/usr/local/share/aisbf'))
add(Path('/usr/share/aisbf')) add(Path('/usr/share/aisbf'))
add(Path.home() / '.local' / 'share' / 'aisbf') add(Path.home() / '.local' / 'share' / 'aisbf')
...@@ -65,12 +78,10 @@ def _share_dir_candidates(): ...@@ -65,12 +78,10 @@ def _share_dir_candidates():
def _share_dir_is_complete(d): def _share_dir_is_complete(d):
"""Return True only if d contains every file/dir the server needs."""
return all((d / f).exists() for f in _REQUIRED_FILES + _REQUIRED_DIRS) return all((d / f).exists() for f in _REQUIRED_FILES + _REQUIRED_DIRS)
def _find_share_dir(): def _find_share_dir():
"""Return the first complete share directory found, or None."""
for candidate in _share_dir_candidates(): for candidate in _share_dir_candidates():
if _share_dir_is_complete(candidate): if _share_dir_is_complete(candidate):
return candidate return candidate
...@@ -79,20 +90,21 @@ def _find_share_dir(): ...@@ -79,20 +90,21 @@ def _find_share_dir():
def _pkg_bundle_dir(): def _pkg_bundle_dir():
""" """
Return the aisbf/_share/ directory bundled inside the installed package, Locate the bundle of runtime files shipped inside the aisbf package.
or None if it doesn't exist (e.g. editable source install before build).
Falls back to the project root for editable installs. Two sources, tried in order:
1. aisbf/_share/ — populated by the build_py hook at wheel-build time
2. project root — for editable (pip install -e) installs
""" """
try: try:
import aisbf as _pkg import aisbf as _pkg
pkg_dir = Path(_pkg.__file__).parent pkg_dir = Path(_pkg.__file__).parent
# Normal wheel install: _share/ populated by build_py hook
share = pkg_dir / '_share' share = pkg_dir / '_share'
if share.exists() and (share / 'main.py').exists(): if share.is_dir() and (share / 'main.py').exists():
return share return share
# Editable install: files live in the project root (one level up) # Editable install: package lives inside the source tree
project_root = pkg_dir.parent project_root = pkg_dir.parent
if (project_root / 'main.py').exists(): if (project_root / 'main.py').exists():
return project_root return project_root
...@@ -104,16 +116,73 @@ def _pkg_bundle_dir(): ...@@ -104,16 +116,73 @@ def _pkg_bundle_dir():
def _bootstrap_share_dir(): def _bootstrap_share_dir():
""" """
Copy all runtime files from the bundled package data to Copy all runtime files from the package bundle to the share directory.
~/.local/share/aisbf/ and return the destination path, or None on failure. Prints step-by-step progress so the first run is fully traceable.
Returns the destination Path on success, None on failure.
""" """
bundle = _pkg_bundle_dir() _log = lambda msg: print(f"[aisbf bootstrap] {msg}", file=sys.stderr)
_log("First run — setting up share directory …")
_log(f" Python : {sys.executable} ({sys.version.split()[0]})")
_log(f" Running as : {'root (uid=0)' if _is_root() else f'uid={os.getuid()}'}")
_log("")
# ── sysconfig paths ──────────────────────────────────────────────────────
_log("sysconfig data paths:")
for scheme in (None, 'posix_user', 'posix_prefix', 'posix_home'):
try:
p = sysconfig.get_path('data', scheme) if scheme else sysconfig.get_path('data')
_log(f" {scheme or 'default':15s}: {p}")
except Exception as e:
_log(f" {scheme or 'default':15s}: ERROR — {e}")
_log("")
# ── candidate share dirs ──────────────────────────────────────────────────
_log("Candidate share directories:")
for c in _share_dir_candidates():
has_sh = (c / 'aisbf.sh').exists()
has_main = (c / 'main.py').exists()
status = "COMPLETE" if _share_dir_is_complete(c) else \
f"partial (aisbf.sh={has_sh}, main.py={has_main})"
_log(f" {c}: {status}")
_log("")
# ── package bundle ────────────────────────────────────────────────────────
_log("Package bundle location:")
try:
import aisbf as _pkg
pkg_dir = Path(_pkg.__file__).parent
_log(f" Package dir : {pkg_dir}")
share = pkg_dir / '_share'
_log(f" _share exists: {share.exists()}")
if share.is_dir():
contents = sorted(p.name for p in share.iterdir())
_log(f" _share contents: {contents}")
bundle = _pkg_bundle_dir()
_log(f" Bundle source: {bundle}")
except Exception as e:
_log(f" ERROR inspecting package: {e}")
bundle = None
_log("")
if bundle is None: if bundle is None:
_log("ERROR: No bundle source found.")
_log(" The wheel was probably built before the build_py hook was added.")
_log(" Rebuild the wheel from the current source and reinstall:")
_log(" python -m build")
_log(" pip install dist/aisbf-*.whl --force-reinstall")
return None return None
dest = Path.home() / '.local' / 'share' / 'aisbf' # ── copy files ────────────────────────────────────────────────────────────
dest = _default_share_dest()
_log(f"Destination : {dest}")
try: try:
dest.mkdir(parents=True, exist_ok=True) dest.mkdir(parents=True, exist_ok=True)
_log(f" Created directory: {dest}")
for fname in _REQUIRED_FILES: for fname in _REQUIRED_FILES:
src = bundle / fname src = bundle / fname
...@@ -122,19 +191,35 @@ def _bootstrap_share_dir(): ...@@ -122,19 +191,35 @@ def _bootstrap_share_dir():
if fname.endswith('.sh'): if fname.endswith('.sh'):
p = dest / fname p = dest / fname
p.chmod(p.stat().st_mode | 0o755) p.chmod(p.stat().st_mode | 0o755)
_log(f" Copied : {fname}")
else:
_log(f" MISSING: {fname} (not in bundle — bundle may be incomplete)")
for dname in _REQUIRED_DIRS: for dname in _REQUIRED_DIRS:
src = bundle / dname src = bundle / dname
dst = dest / dname dst = dest / dname
if src.exists(): if src.is_dir():
if dst.exists(): if dst.exists():
shutil.rmtree(dst) shutil.rmtree(dst)
shutil.copytree(src, dst) shutil.copytree(src, dst)
n = sum(1 for _ in dst.rglob('*') if _.is_file())
return dest if _share_dir_is_complete(dest) else None _log(f" Copied : {dname}/ ({n} files)")
else:
_log(f" MISSING: {dname}/ (not in bundle)")
if _share_dir_is_complete(dest):
_log(f"SUCCESS: Share directory is ready at {dest}")
return dest
missing = [f for f in _REQUIRED_FILES + _REQUIRED_DIRS
if not (dest / f).exists()]
_log(f"INCOMPLETE after copy — still missing: {missing}")
return None
except Exception as e: except Exception as e:
print(f"Warning: could not bootstrap share directory: {e}", file=sys.stderr) _log(f"ERROR during copy: {e}")
import traceback
traceback.print_exc(file=sys.stderr)
return None return None
...@@ -142,31 +227,22 @@ def main(): ...@@ -142,31 +227,22 @@ def main():
share_dir = _find_share_dir() share_dir = _find_share_dir()
if share_dir is None: if share_dir is None:
share_dir = _bootstrap_share_dir()
if share_dir is None:
checked = '\n'.join(f' - {p}' for p in _share_dir_candidates())
dest = _default_share_dest()
print( print(
"AISBF share directory not found or incomplete — bootstrapping from package data …", f"\nError: could not set up the AISBF share directory at {dest}.\n\n"
"Locations checked:\n"
f"{checked}\n\n"
"The wheel was likely built before the build_py hook was added.\n"
"Rebuild from the current source and reinstall:\n"
" python -m build\n"
" pip install dist/aisbf-*.whl --force-reinstall",
file=sys.stderr, file=sys.stderr,
) )
share_dir = _bootstrap_share_dir() sys.exit(1)
if share_dir:
print(
f"Bootstrapped to {share_dir}. Future runs will use this location.",
file=sys.stderr,
)
else:
checked = '\n'.join(f' - {p}' for p in _share_dir_candidates())
print(
"Error: could not set up the AISBF share directory.\n\n"
"Checked locations:\n"
f"{checked}\n\n"
"The wheel may have been built without runtime files.\n"
"Re-install from a source distribution to fix this:\n"
" pip install aisbf --no-binary aisbf\n"
"Or install system-wide as root:\n"
" sudo pip install aisbf",
file=sys.stderr,
)
sys.exit(1)
script_path = share_dir / 'aisbf.sh' script_path = share_dir / 'aisbf.sh'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment