Skip to content

Commit b4178a8

Browse files
fix: enhance environment detection for jac binary and version retrieval
1 parent 044d4b9 commit b4178a8

2 files changed

Lines changed: 88 additions & 17 deletions

File tree

src/utils/envDetection.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async function getJacInEnv(envPath: string): Promise<string | null> {
3636
return (await fileExists(jacPath)) ? jacPath : null;
3737
}
3838

39-
// Walks directories looking for venvs (identified by pyvenv.cfg) with jac installed
39+
// Walks dirs for venvs (pyvenv.cfg). Skips .jac/ — those are project plugin venvs.
4040
async function walkForVenvs(baseDir: string, depth: number): Promise<string[]> {
4141
if (depth === 0) return [];
4242

@@ -49,7 +49,7 @@ async function walkForVenvs(baseDir: string, depth: number): Promise<string[]> {
4949

5050
const results = await Promise.all(
5151
entries
52-
.filter(dirEntry => dirEntry.isDirectory())
52+
.filter(dirEntry => dirEntry.isDirectory() && dirEntry.name !== '.jac')
5353
.map(async (dirEntry): Promise<string[]> => {
5454
const fullPath = path.join(baseDir, dirEntry.name);
5555
if (await isVenv(fullPath)) {
@@ -140,9 +140,30 @@ async function findInCondaEnvs(): Promise<string[]> {
140140
return jacResults.filter((result): result is string => result !== null);
141141
}
142142

143-
// Locator 3: Finds venvs in workspace
143+
// Locator 3: Finds venvs + zig-out dev builds in workspace.
144+
// Checks zig-out/bin/jac at root and one level deep (covers both jaseci/ and jaseci/jac/ as workspace).
144145
async function findInWorkspace(workspaceRoot: string): Promise<string[]> {
145-
return walkForVenvs(workspaceRoot, VENV_WALK_DEPTH);
146+
const venvs = await walkForVenvs(workspaceRoot, VENV_WALK_DEPTH);
147+
148+
const jacExe = process.platform === 'win32' ? 'jac.exe' : 'jac';
149+
const zigOutPaths: string[] = [];
150+
151+
const checkZigOut = async (dir: string) => {
152+
const candidate = path.join(dir, 'zig-out', 'bin', jacExe);
153+
if (await fileExists(candidate)) zigOutPaths.push(candidate);
154+
};
155+
156+
await checkZigOut(workspaceRoot);
157+
let entries: Dirent[];
158+
try { entries = await fs.readdir(workspaceRoot, { withFileTypes: true }); }
159+
catch { entries = []; }
160+
await Promise.all(
161+
entries
162+
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
163+
.map(e => checkZigOut(path.join(workspaceRoot, e.name)))
164+
);
165+
166+
return [...venvs, ...zigOutPaths];
146167
}
147168

148169
// Locator 4: Finds venvs in home directory stores
@@ -196,7 +217,18 @@ export async function discoverJacEnvironments(workspaceRoots: string[]): Promise
196217
condaEnvs.forEach(envPath => add(envPath, 'conda'));
197218
homeEnvs.forEach(envPath => add(envPath, 'venv'));
198219

199-
return envs;
220+
// Dedup by realpath: --dev install symlinks ~/.local/bin/jac → zig-out/bin/jac.
221+
const seenReal = new Set<string>();
222+
const deduped: JacEnvironment[] = [];
223+
for (const env of envs) {
224+
let real: string;
225+
try { real = await fs.realpath(env.path); } catch { real = env.path; }
226+
if (!seenReal.has(real)) {
227+
seenReal.add(real);
228+
deduped.push(env);
229+
}
230+
}
231+
return deduped;
200232
}
201233

202234
// ── Validation ───────────────────────────────────────────────────────────────

src/utils/envVersion.ts

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as path from 'path';
33
import * as os from 'os';
44
import { execFile } from 'child_process';
55
import { promisify } from 'util';
6+
import { createHash } from 'crypto';
67

78
const execFileAsync = promisify(execFile);
89

@@ -46,23 +47,61 @@ async function getJacVersionFromDistInfo(jacPath: string): Promise<string | unde
4647
} catch { return undefined; }
4748
}
4849

49-
// Middle path: scan ~/.cache/jac/rt/*/site/jaclang-*.dist-info — no subprocess, works for binary installs.
50-
async function getJacVersionFromCache(): Promise<string | undefined> {
50+
// Middle path: scan ~/.cache/jac/rt/*/site/jaclang-*.dist-info, keyed on jacPath.
51+
// New format (≥0.30.3): <hash16>-<pathhash16> — per binary. Old format (≤0.30.2): <hash16> — shared.
52+
// Old format used only when no new-format dirs exist (prevents cross-version pollution).
53+
async function getJacVersionFromCache(jacPath: string): Promise<string | undefined> {
5154
const cacheBase = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache');
5255
const rtDir = path.join(cacheBase, 'jac', 'rt');
5356
let hashDirs: string[];
5457
try { hashDirs = await fs.readdir(rtDir); }
5558
catch { return undefined; }
56-
const versions: string[] = [];
57-
for (const hashDir of hashDirs) {
58-
let entries: string[];
59-
try { entries = await fs.readdir(path.join(rtDir, hashDir, 'site')); }
60-
catch { continue; }
61-
const distInfo = entries.find(e => e.startsWith('jaclang-') && e.endsWith('.dist-info'));
62-
if (distInfo) versions.push(distInfo.slice('jaclang-'.length, -'.dist-info'.length));
59+
if (hashDirs.length === 0) return undefined;
60+
61+
const RT_KEY_LEN = 33;
62+
const HEX16 = /^[0-9a-f]{16}$/;
63+
64+
// Runtime hashes realpath(exe); try resolved first, fall back to raw (macOS symlink behaviour).
65+
let resolvedPath: string;
66+
try { resolvedPath = await fs.realpath(jacPath); } catch { resolvedPath = jacPath; }
67+
68+
const pathHashFor = (p: string) => createHash('sha256').update(p).digest('hex').slice(0, 16);
69+
70+
// New format: per-binary
71+
const newFormatDirs = hashDirs.filter(d => d.length === RT_KEY_LEN && d[16] === '-');
72+
let matchingDirs = newFormatDirs.filter(d => d.slice(17) === pathHashFor(resolvedPath));
73+
if (matchingDirs.length === 0 && resolvedPath !== jacPath) {
74+
matchingDirs = newFormatDirs.filter(d => d.slice(17) === pathHashFor(jacPath));
75+
}
76+
77+
if (matchingDirs.length > 0) {
78+
const versions: string[] = [];
79+
for (const dir of matchingDirs) {
80+
let entries: string[];
81+
try { entries = await fs.readdir(path.join(rtDir, dir, 'site')); } catch { continue; }
82+
const di = entries.find(e => e.startsWith('jaclang-') && e.endsWith('.dist-info'));
83+
if (di) versions.push(di.slice('jaclang-'.length, -'.dist-info'.length));
84+
}
85+
const unique = [...new Set(versions)];
86+
if (unique.length === 1) return unique[0];
87+
return undefined;
88+
}
89+
90+
// Old format: only when no new-format dirs exist (avoids returning stale version after GC).
91+
const oldFormatDirs = hashDirs.filter(d => d.length === 16 && HEX16.test(d));
92+
if (newFormatDirs.length === 0 && oldFormatDirs.length > 0) {
93+
const versions: string[] = [];
94+
for (const dir of oldFormatDirs) {
95+
let entries: string[];
96+
try { entries = await fs.readdir(path.join(rtDir, dir, 'site')); } catch { continue; }
97+
const di = entries.find(e => e.startsWith('jaclang-') && e.endsWith('.dist-info'));
98+
if (di) versions.push(di.slice('jaclang-'.length, -'.dist-info'.length));
99+
}
100+
const unique = [...new Set(versions)];
101+
if (unique.length === 1) return unique[0];
63102
}
64-
const unique = [...new Set(versions)];
65-
return unique.length === 1 ? unique[0] : undefined;
103+
104+
return undefined;
66105
}
67106

68107
// Last resort: ask the specific binary directly (~1.8s, may be slow on cold cache).
@@ -75,7 +114,7 @@ async function getJacVersionFromBinary(jacPath: string): Promise<string | undefi
75114

76115
// dist-info (venvs) → cache scan (binary install, no subprocess) → subprocess (last resort).
77116
export async function getJacVersion(jacPath: string): Promise<string | undefined> {
78-
return (await getJacVersionFromDistInfo(jacPath)) ?? (await getJacVersionFromCache()) ?? (await getJacVersionFromBinary(jacPath));
117+
return (await getJacVersionFromDistInfo(jacPath)) ?? (await getJacVersionFromCache(jacPath)) ?? (await getJacVersionFromBinary(jacPath));
79118
}
80119

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

0 commit comments

Comments
 (0)