|
23 | 23 | 5. STATE THE BOUND. Say what the answer cannot see — the relations this graph does not encode — so a |
24 | 24 | partial list is not read as a complete one. |
25 | 25 | """ |
26 | | -import collections, difflib, os, re, subprocess, sys |
| 26 | +import collections, difflib, os, re, subprocess, sys, time |
27 | 27 |
|
28 | 28 | SPLIT = re.compile(r'[^A-Za-z0-9]+') |
29 | 29 | CAMEL = re.compile(r'[A-Z]+(?![a-z])|[A-Z][a-z0-9]*|[a-z0-9]+') |
@@ -384,13 +384,87 @@ def ensure_graph(repo, db): |
384 | 384 | if not os.environ.get('AXIOMCODE_AUTOBUILD') or os.environ.get('AXIOMCODE_GRAPH'): return False |
385 | 385 | build = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'axiomcode-build') |
386 | 386 | if not os.path.exists(build): return False |
| 387 | + import ax_fresh |
| 388 | + bash = os.environ.get('AXIOMCODE_BASH') or 'bash' |
| 389 | + # NEVER A SILENT WAIT. A first build takes minutes (a whole-program solve, and on a machine that has never built this |
| 390 | + # language, a one-time compile of its rules), and it used to run with nothing said after its first line, so a caller |
| 391 | + # could not tell a build from a hang. Under the MCP server (AXIOMCODE_BUILD_NOWAIT) the build is started in the |
| 392 | + # background and the call answers within AXIOMCODE_BUILD_WAIT seconds either way: with the graph when it was quick, |
| 393 | + # else with the stage the build is at, so the agent is told to ask again rather than left waiting past the host's |
| 394 | + # timeout. From a shell the call waits for it, printing the stage as it moves. A call that finds a build already |
| 395 | + # running (another query's, or an `axiomcode index`) reports on that one rather than queueing a second behind it. |
| 396 | + nowait = bool(os.environ.get('AXIOMCODE_BUILD_NOWAIT')) |
| 397 | + if ax_fresh.building(repo): |
| 398 | + if nowait: _BUILD_NOTE.append(building_note(repo)); return False |
| 399 | + print(f"a graph build is already running for {repo}; waiting for it …", file=sys.stderr) |
| 400 | + _follow(repo, lambda: ax_fresh.building(repo)) |
| 401 | + return os.path.exists(db) |
387 | 402 | print(f"no graph for {repo} yet — building one (this is the only slow call; later ones read it) …", file=sys.stderr) |
388 | | - r = subprocess.run([os.environ.get('AXIOMCODE_BASH') or 'bash', build, repo]) |
389 | | - return r.returncode == 0 and os.path.exists(db) |
| 403 | + if nowait: |
| 404 | + os.makedirs(os.path.join(repo, '.axiomcode'), exist_ok=True) |
| 405 | + log = open(os.path.join(repo, '.axiomcode', 'first-build.log'), 'w') |
| 406 | + kw = dict(start_new_session=True) if os.name != 'nt' else dict(creationflags=0x00000008 | 0x00000200) # DETACHED | NEW_GROUP, as ax_fresh.kick |
| 407 | + p = subprocess.Popen([bash, build, repo], stdin=subprocess.DEVNULL, stdout=log, stderr=log, close_fds=True, **kw) |
| 408 | + # until the build holds its lock a second query would see no build and start another, so that much is always waited |
| 409 | + up = time.time() + 10 |
| 410 | + while p.poll() is None and not ax_fresh.building(repo) and time.time() < up: time.sleep(0.05) |
| 411 | + end = time.time() + float(os.environ.get('AXIOMCODE_BUILD_WAIT') or 60) |
| 412 | + while p.poll() is None and time.time() < end: time.sleep(0.5) |
| 413 | + if p.poll() is None: _BUILD_NOTE.append(building_note(repo)); return False |
| 414 | + return p.returncode == 0 and os.path.exists(db) |
| 415 | + p = subprocess.Popen([bash, build, repo]) |
| 416 | + _follow(repo, lambda: p.poll() is None) |
| 417 | + return p.returncode == 0 and os.path.exists(db) |
| 418 | + |
| 419 | + |
| 420 | +_BUILD_NOTE = [] |
| 421 | +TICK = 20.0 |
| 422 | + |
| 423 | + |
| 424 | +def build_stage(repo): |
| 425 | + """(stage, done, of): the last `▶` step the running build's log names, and how many of its languages it has |
| 426 | + started solving, from .axiomcode/build.log""" |
| 427 | + stage, langs, solving = '', [], 0 |
| 428 | + try: text = open(os.path.join(repo, '.axiomcode', 'build.log'), errors='replace').read() |
| 429 | + except OSError: text = '' |
| 430 | + for l in text.splitlines(): |
| 431 | + if not l.startswith('\u25b6 '): continue |
| 432 | + stage = l[2:].strip() |
| 433 | + if stage.startswith('languages:'): langs = stage.split(':', 1)[1].split() |
| 434 | + elif stage.startswith('solving '): solving += 1 |
| 435 | + return stage, solving, len(langs) |
| 436 | + |
| 437 | + |
| 438 | +def building_note(repo): |
| 439 | + """one line: a graph is being built, what it is doing and for how long, and what to do meanwhile""" |
| 440 | + try: took = time.time() - os.path.getmtime(os.path.join(repo, '.axiomcode', 'build.lock')) |
| 441 | + except OSError: took = 0 |
| 442 | + stage, n, of = build_stage(repo) |
| 443 | + where = (f"language {n} of {of}, " if of and n else '') + (stage or 'starting') |
| 444 | + if 'compiling souffle program' in stage: |
| 445 | + where += ' — a one-time compile of this language\'s rules on this machine, a few minutes; later builds reuse it' |
| 446 | + return (f"the graph for {repo} is being built ({where}; {int(took // 60)}m{int(took % 60):02d}s so far). " |
| 447 | + f"Nothing else is needed: ask again in a minute or two, or read the code directly meanwhile. " |
| 448 | + f"Progress: {os.path.join(repo, '.axiomcode', 'build.log')}") |
| 449 | + |
| 450 | + |
| 451 | +def _follow(repo, running): |
| 452 | + """wait while running(), printing the build's stage to stderr whenever it moves, and every TICK seconds at least""" |
| 453 | + last, said = None, time.time() |
| 454 | + while running(): |
| 455 | + time.sleep(0.5) |
| 456 | + stage, n, of = build_stage(repo) |
| 457 | + if (stage and stage != last) or time.time() - said >= TICK: |
| 458 | + try: took = time.time() - os.path.getmtime(os.path.join(repo, '.axiomcode', 'build.lock')) |
| 459 | + except OSError: took = 0 |
| 460 | + print(f" … {'language %d of %d, ' % (n, of) if of and n else ''}{stage or 'starting'} ({int(took)}s)", file=sys.stderr, flush=True) |
| 461 | + last, said = stage, time.time() |
390 | 462 |
|
391 | 463 |
|
392 | 464 | def no_graph(repo, db): |
393 | 465 | """what to say when there is still no graph after ensure_graph has had its turn.""" |
| 466 | + if _BUILD_NOTE: |
| 467 | + return _BUILD_NOTE[-1] |
394 | 468 | if os.environ.get('AXIOMCODE_GRAPH'): |
395 | 469 | return f"no graph at {db} (AXIOMCODE_GRAPH is set, so nothing was built into it)" |
396 | 470 | if os.environ.get('AXIOMCODE_AUTOBUILD'): |
|
0 commit comments