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

parent 23a9f726
......@@ -29,12 +29,22 @@ import shutil
from pathlib import Path
# Files and directories that must be present in the share directory for the
# server to start correctly.
# Files and directories the share directory must contain for the server to start.
_REQUIRED_FILES = ['aisbf.sh', 'main.py', 'requirements.txt']
_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():
"""Return candidate share directories in priority order, deduplicated."""
seen = set()
......@@ -46,9 +56,12 @@ def _share_dir_candidates():
seen.add(key)
result.append(Path(p))
# sysconfig paths for the running Python interpreter — covers venvs,
# system installs, and user installs regardless of pip's install scheme.
# sysconfig paths for the running interpreter — covers venvs, system
# installs, and all user-install schemes regardless of how pip was invoked.
try:
add(Path(sysconfig.get_path('data')) / 'share' / 'aisbf')
except Exception:
pass
for scheme in ('posix_user', 'posix_prefix', 'posix_home'):
try:
......@@ -56,7 +69,7 @@ def _share_dir_candidates():
except Exception:
pass
# Legacy hardcoded fallbacks
# Hardcoded legacy fallbacks
add(Path('/usr/local/share/aisbf'))
add(Path('/usr/share/aisbf'))
add(Path.home() / '.local' / 'share' / 'aisbf')
......@@ -65,12 +78,10 @@ def _share_dir_candidates():
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)
def _find_share_dir():
"""Return the first complete share directory found, or None."""
for candidate in _share_dir_candidates():
if _share_dir_is_complete(candidate):
return candidate
......@@ -79,20 +90,21 @@ def _find_share_dir():
def _pkg_bundle_dir():
"""
Return the aisbf/_share/ directory bundled inside the installed package,
or None if it doesn't exist (e.g. editable source install before build).
Falls back to the project root for editable installs.
Locate the bundle of runtime files shipped inside the aisbf package.
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:
import aisbf as _pkg
pkg_dir = Path(_pkg.__file__).parent
# Normal wheel install: _share/ populated by build_py hook
share = pkg_dir / '_share'
if share.exists() and (share / 'main.py').exists():
if share.is_dir() and (share / 'main.py').exists():
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
if (project_root / 'main.py').exists():
return project_root
......@@ -104,16 +116,73 @@ def _pkg_bundle_dir():
def _bootstrap_share_dir():
"""
Copy all runtime files from the bundled package data to
~/.local/share/aisbf/ and return the destination path, or None on failure.
Copy all runtime files from the package bundle to the share directory.
Prints step-by-step progress so the first run is fully traceable.
Returns the destination Path on success, None on failure.
"""
_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:
_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
dest = Path.home() / '.local' / 'share' / 'aisbf'
# ── copy files ────────────────────────────────────────────────────────────
dest = _default_share_dest()
_log(f"Destination : {dest}")
try:
dest.mkdir(parents=True, exist_ok=True)
_log(f" Created directory: {dest}")
for fname in _REQUIRED_FILES:
src = bundle / fname
......@@ -122,19 +191,35 @@ def _bootstrap_share_dir():
if fname.endswith('.sh'):
p = dest / fname
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:
src = bundle / dname
dst = dest / dname
if src.exists():
if src.is_dir():
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst)
n = sum(1 for _ in dst.rglob('*') if _.is_file())
_log(f" Copied : {dname}/ ({n} files)")
else:
_log(f" MISSING: {dname}/ (not in bundle)")
return dest if _share_dir_is_complete(dest) else None
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:
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
......@@ -142,28 +227,19 @@ def main():
share_dir = _find_share_dir()
if share_dir is None:
print(
"AISBF share directory not found or incomplete — bootstrapping from package data …",
file=sys.stderr,
)
share_dir = _bootstrap_share_dir()
if share_dir:
print(
f"Bootstrapped to {share_dir}. Future runs will use this location.",
file=sys.stderr,
)
else:
if share_dir is None:
checked = '\n'.join(f' - {p}' for p in _share_dir_candidates())
dest = _default_share_dest()
print(
"Error: could not set up the AISBF share directory.\n\n"
"Checked locations:\n"
f"\nError: could not set up the AISBF share directory at {dest}.\n\n"
"Locations checked:\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",
"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,
)
sys.exit(1)
......
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