@@ -3,6 +3,7 @@ import * as path from 'path';
33import * as os from 'os' ;
44import { execFile } from 'child_process' ;
55import { promisify } from 'util' ;
6+ import { createHash } from 'crypto' ;
67
78const 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 - 9 a - 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).
77116export 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