"""Build the jobbsok Python environment and pin the ingestion guard. Python rather than bash, and standard library only. The shell version made the first step of the install impossible on stock Windows, which is the one step nobody can skip -- without it there is no interpreter for the host tool server and no guard for M3 ingestion. Like the launcher, this file is written to parse on Python 3.7: it may be started by exactly the old interpreter it refuses, and a refusal that is a SyntaxError is not a refusal. Where the environment lives is load-bearing, not incidental. When CLAUDE_PLUGIN_DATA is set the environment is built there, because that directory survives a plugin update -- the plugin's own cache does not. A repo-root-only .venv would leave every non-development install without the guard, which is risk H2. The repo-root .venv is the development case. One port decision, stated rather than smuggled: the shell version defaulted to `python3` on PATH, and Windows has no such name. The default here is the interpreter that started this file, which is the same intent and is more predictable than a PATH lookup -- and it is gated on the version exactly as before, so an install started on 3.9.6 is still refused rather than built. Usage: python3 scripts/bootstrap.py # dev dependencies python3 scripts/bootstrap.py --med-xlsx # also the openpyxl extra On Windows the interpreter is normally called `python`, not `python3`. """ import os import shutil import subprocess import sys MIN_MAJOR = 3 MIN_MINOR = 10 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) REPO_ROOT = os.path.dirname(SCRIPT_DIR) class Avvist(Exception): """An interpreter that cannot be built on, and the words to say so.""" def __init__(self, linjer): Exception.__init__(self, linjer[0]) self.linjer = linjer def parse_args(argv): """Return True when the xlsx extra was asked for; exit as the shell did.""" med_xlsx = False for arg in argv: if arg == "--med-xlsx": med_xlsx = True elif arg in ("-h", "--help"): sys.stdout.write(__doc__) raise SystemExit(0) else: sys.stderr.write("bootstrap: unknown argument: %s\n" % arg) sys.stderr.write( "bootstrap: usage: python3 scripts/bootstrap.py [--med-xlsx]\n" ) raise SystemExit(2) return med_xlsx def venv_location(env, repo_root): """Return (directory, what kind of directory it is) for the environment.""" plugin_data = env.get("CLAUDE_PLUGIN_DATA") if plugin_data: return ( os.path.join(plugin_data, "venv"), "plugin-data (survives a plugin update)", ) return os.path.join(repo_root, ".venv"), "repo root (development)" def venv_python(venv_dir, os_name=None): """The interpreter inside a virtualenv, in the layout this platform uses.""" if os_name is None: os_name = os.name if os_name == "nt": return os.path.join(venv_dir, "Scripts", "python.exe") return os.path.join(venv_dir, "bin", "python") def version_ok(tolk): """True when ``tolk`` reports at least the floor.""" return 0 == subprocess.call( [ tolk, "-c", "import sys; sys.exit(0 if sys.version_info >= (%d, %d) else 1)" % (MIN_MAJOR, MIN_MINOR), ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) def version_of(tolk): """The version ``tolk`` reports, or "ukjent" when it will not say.""" try: ut = subprocess.run( [tolk, "-c", "import sys; print('%d.%d.%d' % sys.version_info[:3])"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) except OSError: return "ukjent" if ut.returncode != 0 or not ut.stdout.strip(): return "ukjent" return ut.stdout.strip() def resolve_interpreter(env, which=None, gate=None): """The interpreter to build the environment on. Refuses rather than building an environment that parses everything and fails at runtime: under a GUI-spawned process with an empty PATH, the interpreter here can be 3.9.6. """ if which is None: which = shutil.which if gate is None: gate = version_ok navngitt = env.get("JOBBSOK_PYTHON") tolk = navngitt or sys.executable funnet = which(tolk) if not funnet: raise Avvist( [ "bootstrap: no such interpreter: %s" % tolk, "bootstrap: set JOBBSOK_PYTHON to a Python %d.%d+ executable." % (MIN_MAJOR, MIN_MINOR), ] ) if not gate(funnet): raise Avvist( [ "bootstrap: %s is Python %s; %d.%d+ is required." % (tolk, version_of(funnet), MIN_MAJOR, MIN_MINOR), "bootstrap: refusing to build an environment on it. Set " "JOBBSOK_PYTHON instead.", ] ) return funnet def main(argv, env=None): if env is None: env = os.environ med_xlsx = parse_args(argv) try: tolk = resolve_interpreter(env) except Avvist as avvist: for linje in avvist.linjer: sys.stderr.write(linje + "\n") return 1 venv_dir, art = venv_location(env, REPO_ROOT) sys.stdout.write("bootstrap: interpreter %s\n" % tolk) sys.stdout.write("bootstrap: environment %s [%s]\n" % (venv_dir, art)) venv_py = venv_python(venv_dir) if not os.path.isfile(venv_py): forelder = os.path.dirname(venv_dir) if forelder and not os.path.isdir(forelder): os.makedirs(forelder) kode = subprocess.call([tolk, "-m", "venv", venv_dir]) if kode != 0: return kode pyproject = os.path.join(REPO_ROOT, "pyproject.toml") # PEP 735 dependency groups need pip 25.1+; the upgrade is what makes the # --group line below safe, and a pip too old to understand it fails loudly. trinn = [ [venv_py, "-m", "pip", "install", "--quiet", "--upgrade", "pip"], [venv_py, "-m", "pip", "install", "--quiet", "--group", pyproject + ":dev"], ] if med_xlsx: trinn.append( [venv_py, "-m", "pip", "install", "--quiet", "--group", pyproject + ":xlsx"] ) for kommando in trinn: kode = subprocess.call(kommando) if kode != 0: return kode rapport = ( "import sys\n" "import llm_ingestion_guard\n" "print('bootstrap: sys.executable %s' % sys.executable)\n" "print('bootstrap: python %s' % sys.version.split()[0])\n" "print('bootstrap: guard %s' % llm_ingestion_guard.__version__)\n" ) kode = subprocess.call([venv_py, "-c", rapport]) if kode != 0: return kode sys.stdout.write("bootstrap: ok\n") return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))