Skip to content

Commit ef2fc2f

Browse files
authored
fix: report each environment's own jaclang version (#98)
The cache fast-path scanned the global ~/.cache/jac/rt runtime cache, which is independent of the env being queried, so every environment in the picker showed the same version (the last jac that ran, e.g. 0.30.2) even when conda envs and venvs actually had older jaclang (0.15.0, 0.13.x). Resolve the version per jacPath instead: scan that env's own site-packages dist-info (gated on pyvenv.cfg so a single binary's shared ~/.local prefix does not match an unrelated pip install), then fall back to the binary's own 'jac --version'. Each environment now reports its true version, and the single binary reports its real version rather than a stale ~/.local pip leftover.
1 parent d7cb481 commit ef2fc2f

1 file changed

Lines changed: 39 additions & 27 deletions

File tree

src/utils/envVersion.ts

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,62 @@
11
import * as fs from 'fs/promises';
22
import * as path from 'path';
3-
import * as os from 'os';
43
import { execFile } from 'child_process';
54
import { promisify } from 'util';
65

76
const execFileAsync = promisify(execFile);
87

9-
// Fast path: read version from ~/.cache/jac/rt/*/site/jaclang-*.dist-info (~0.002s).
10-
async function getJacVersionFromCache(): Promise<string | undefined> {
11-
const cacheBase = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache');
12-
const rtDir = path.join(cacheBase, 'jac', 'rt');
8+
// Matches the version on `jac --version` output, e.g. "Version: 0.30.2".
9+
const VERSION_RE = /Version:\s*([0-9]+\.[0-9]+\.[0-9]+(?:[.\-+][0-9A-Za-z.\-]+)?)/;
1310

14-
let hashDirs: string[];
15-
try { hashDirs = await fs.readdir(rtDir); }
16-
catch { return undefined; }
11+
// Fast path: scan THIS env's own site-packages for a jaclang-*.dist-info (~1ms, no subprocess).
12+
// Keyed on the given jacPath so each env reports its own version. A single self-contained
13+
// binary has no sibling dist-info, so this returns undefined and the subprocess path is used.
14+
async function getJacVersionFromDistInfo(jacPath: string): Promise<string | undefined> {
15+
try {
16+
const envRoot = path.dirname(path.dirname(jacPath));
1717

18-
const versions: string[] = [];
18+
// Only trust the dist-info scan inside a real venv/conda env (marked by pyvenv.cfg).
19+
// A single binary at e.g. ~/.local/bin shares its prefix with unrelated pip installs,
20+
// so its sibling lib/ dist-info would be a different, stale jaclang. Skip it there.
21+
try { await fs.access(path.join(envRoot, 'pyvenv.cfg')); }
22+
catch { return undefined; }
1923

20-
for (const hashDir of hashDirs) {
21-
const siteDir = path.join(rtDir, hashDir, 'site');
22-
let entries: string[];
23-
try { entries = await fs.readdir(siteDir); }
24-
catch { continue; }
24+
const libDir = path.join(envRoot, 'lib');
2525

26-
const distInfo = entries.find(e => e.startsWith('jaclang-') && e.endsWith('.dist-info'));
27-
if (distInfo) {
28-
versions.push(distInfo.slice('jaclang-'.length, -'.dist-info'.length));
29-
}
30-
}
26+
let libEntries: string[];
27+
try { libEntries = await fs.readdir(libDir); }
28+
catch { return undefined; }
3129

32-
// Ambiguous if multiple distinct versions (e.g. dev + release) — fall back to subprocess.
33-
const unique = [...new Set(versions)];
34-
return unique.length === 1 ? unique[0] : undefined;
30+
for (const libEntry of libEntries.filter(entry => entry.startsWith('python'))) {
31+
for (const pkgDir of ['site-packages', 'dist-packages']) {
32+
try {
33+
const sitePackages = path.join(libDir, libEntry, pkgDir);
34+
const siteEntries = await fs.readdir(sitePackages);
35+
const distInfoDir = siteEntries.find(
36+
entry => entry.startsWith('jaclang-') && entry.endsWith('.dist-info')
37+
);
38+
if (distInfoDir) {
39+
return distInfoDir.slice('jaclang-'.length, -'.dist-info'.length);
40+
}
41+
} catch { continue; }
42+
}
43+
}
44+
return undefined;
45+
} catch { return undefined; }
3546
}
3647

37-
// Slow fallback (~1.8s): only runs if cache is missing (first-ever launch).
48+
// Authoritative: ask the specific binary directly. Works for any install (single binary or venv).
3849
async function getJacVersionFromBinary(jacPath: string): Promise<string | undefined> {
3950
try {
40-
const { stdout } = await execFileAsync(jacPath, ['--version'], { timeout: 5000 });
41-
return stdout.match(/Version:\s+([\d.]+)/)?.[1];
51+
const { stdout, stderr } = await execFileAsync(jacPath, ['--version'], { timeout: 5000 });
52+
return VERSION_RE.exec(`${stdout}\n${stderr}`)?.[1];
4253
} catch { return undefined; }
4354
}
4455

45-
// Cache first, subprocess fallback if cache is missing.
56+
// Resolve the version for THIS jacPath. Per-env dist-info first (fast, no subprocess),
57+
// then the binary itself. Both are keyed on jacPath so each env reports its own version.
4658
export async function getJacVersion(jacPath: string): Promise<string | undefined> {
47-
return (await getJacVersionFromCache()) ?? (await getJacVersionFromBinary(jacPath));
59+
return (await getJacVersionFromDistInfo(jacPath)) ?? (await getJacVersionFromBinary(jacPath));
4860
}
4961

5062
// Compares two semver strings. Returns positive if a > b, negative if a < b, 0 if equal.

0 commit comments

Comments
 (0)