Skip to content

Commit 11bc630

Browse files
committed
fix: Fixed jenv implementation (made it so that it's bi-directional and resiliant to homebrew updates. The versions now are stable by using the file location instead of the direct version installed by jenv (since it installs 3 for 17.0.24 -> 17, 17.0, 17.0.24). Added rehashing functionality) + file
1 parent 36c24f3 commit 11bc630

File tree

3 files changed

+140
-63
lines changed

3 files changed

+140
-63
lines changed
Lines changed: 133 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,158 @@
1-
import { ArrayStatefulParameter, getPty } from 'codify-plugin-lib';
1+
import { ArrayParameterSetting, ArrayStatefulParameter, getPty } from 'codify-plugin-lib';
2+
import fs from 'node:fs/promises';
3+
import semver from 'semver';
24

35
import { SpawnStatus, codifySpawn } from '../../../utils/codify-spawn.js';
6+
import { FileUtils } from '../../../utils/file-utils.js';
47
import { Utils } from '../../../utils/index.js';
58
import { JenvConfig } from './jenv.js';
9+
import { nanoid } from 'nanoid';
610

711
export const OPENJDK_SUPPORTED_VERSIONS = [8, 11, 17, 21, 22]
812
export const JAVA_VERSION_INTEGER = /^\d+$/;
913

1014
export class JenvAddParameter extends ArrayStatefulParameter<JenvConfig, string> {
11-
override async refresh(desired: null | string[]): Promise<null | string[]> {
15+
getSettings(): ArrayParameterSetting {
16+
return {
17+
type: 'array',
18+
itemType: 'directory',
19+
isElementEqual: (a, b) => b.includes(a),
20+
transformation: {
21+
to: (input: string[]) =>
22+
input.map((i) => {
23+
if (OPENJDK_SUPPORTED_VERSIONS.includes(Number.parseInt(i, 10))) {
24+
return `/opt/homebrew/Cellar/openjdk@${Number.parseInt(i, 10)}`
25+
}
26+
27+
return i;
28+
}),
29+
from: (output: string[]) => output.map((i) => {
30+
if (i.startsWith('/opt/homebrew/Cellar/openjdk@')) {
31+
return i.split('/').at(4)?.split('@').at(1)
32+
}
33+
34+
return i;
35+
}),
36+
}
37+
}
38+
}
39+
40+
override async refresh(params: string[]): Promise<null | string[]> {
1241
const $ = getPty();
1342

14-
const { data } = await $.spawn('jenv versions')
15-
16-
/** Example:
17-
* system
18-
* * 17 (set by /Users/kevinwang/.jenv/version)
19-
* 17.0
20-
* 17.0.11
21-
* openjdk64-17.0.11
22-
*/
23-
return [...new Set(
24-
data
25-
.split(/\n/)
26-
// Regex to split out the version part
27-
.map((v) => this.getFirstRegexGroup(/^[ *] ([\d.A-Za-z-]+)[ \\n]?/g, v))
28-
.filter(Boolean) as string[]
29-
)]
30-
}
43+
const { data: jenvRoot } = await $.spawn('jenv root')
44+
const versions = (await fs.readdir(`${jenvRoot}/versions`)).filter((v) => v !== '.DS_store');
3145

32-
override async addItem(param: string): Promise<void> {
33-
34-
const isHomebrewInstalled = await Utils.isHomebrewInstalled();
35-
36-
// Add special handling if the user specified an integer version. We add special functionality to automatically
37-
// install java if a lts version is specified and homebrew is installed.
38-
if (JAVA_VERSION_INTEGER.test(param)) {
39-
if (!isHomebrewInstalled) {
40-
throw new Error('Homebrew not detected. Cannot automatically install java version. Jenv does not automatically install' +
41-
' java versions, see the jenv docs: https://www.jenv.be. Please manually install a version of java and provide a path to the jenv resource')
42-
}
43-
44-
const parsedVersion = Number.parseInt(param, 10);
45-
if (!OPENJDK_SUPPORTED_VERSIONS.includes(parsedVersion)) {
46-
throw new Error(`Unsupported version of java specified. Only [${OPENJDK_SUPPORTED_VERSIONS.join(', ')}] is supported`)
47-
}
46+
// We use a set because jenv sets an alias for 11.0.24, 11.0 and 11. We only care about the original location here
47+
const versionPaths = new Set(
48+
await Promise.all(versions.map((v) =>
49+
fs.readlink(`${jenvRoot}/versions/${v}`)
50+
))
51+
)
4852

49-
const openjdkName = (parsedVersion === 22) ? 'openjdk' : `openjdk@${param}`;
50-
const { status } = await codifySpawn(`brew list --formula -1 ${openjdkName}`, { throws: false });
53+
const installedVersions = (await $.spawn('jenv versions --bare'))
54+
.data
55+
.split(/\n/)
5156

52-
// That version is not currently installed with homebrew. Let's install it
53-
if (status === SpawnStatus.ERROR) {
54-
console.log(`Homebrew detected. Attempting to install java version ${openjdkName} automatically using homebrew`)
55-
await codifySpawn(`brew install ${openjdkName}`)
56-
}
57+
return [...versionPaths]
58+
// Re-map the path back to what was provided in the config
59+
.map((v) => {
60+
const matched = params.find((p) => v.includes(p));
61+
return matched === undefined
62+
? v
63+
: matched;
64+
})
65+
.filter((v) => {
66+
const versionStr = v.split('/').at(4)!.split('@').at(1)!;
67+
return installedVersions.includes(versionStr);
68+
});
69+
}
5770

58-
const location = await this.getHomebrewInstallLocation(openjdkName);
59-
if (!location) {
60-
throw new Error('Unable to determine location of jdk installed by homebrew. Please report this to the Codify team');
71+
override async addItem(param: string): Promise<void> {
72+
let location = param;
73+
74+
// Check if we should auto install it from homebrew first
75+
if (param.startsWith('/opt/homebrew/Cellar/openjdk@')) {
76+
77+
// Doesn't currently exist on the file system, let's parse and install from homebrew before adding
78+
if (!(await FileUtils.exists(param))) {
79+
const isHomebrewInstalled = await Utils.isHomebrewInstalled();
80+
if (!isHomebrewInstalled) {
81+
throw new Error('Homebrew not detected. Cannot automatically install java version. Jenv does not automatically install' +
82+
' java versions, see the jenv docs: https://www.jenv.be. Please manually install a version of java and provide a path to the jenv resource')
83+
}
84+
85+
const versionStr = param.split('/').at(4)?.split('@').at(1);
86+
if (!versionStr) {
87+
throw new Error(`jenv: malformed version str: ${versionStr}`)
88+
}
89+
90+
const parsedVersion = Number.parseInt(versionStr, 10)
91+
if (!OPENJDK_SUPPORTED_VERSIONS.includes(parsedVersion)) {
92+
throw new Error(`Unsupported version of java specified. Only [${OPENJDK_SUPPORTED_VERSIONS.join(', ')}] is supported`)
93+
}
94+
95+
const openjdkName = (parsedVersion === 22) ? 'openjdk' : `openjdk@${parsedVersion}`;
96+
const { status } = await codifySpawn(`brew list --formula -1 ${openjdkName}`, { throws: false });
97+
98+
// That version is not currently installed with homebrew. Let's install it
99+
if (status === SpawnStatus.ERROR) {
100+
console.log(`Homebrew detected. Attempting to install java version ${openjdkName} automatically using homebrew`)
101+
await codifySpawn(`brew install ${openjdkName}`)
102+
}
103+
104+
location = (await this.getHomebrewInstallLocation(openjdkName))!;
105+
if (!location) {
106+
throw new Error('Unable to determine location of jdk installed by homebrew. Please report this to the Codify team');
107+
}
108+
109+
// Already exists on the file system let's re-map to the actual path
110+
} else if (!param.endsWith('libexec/openjdk.jdk/Contents/Home')) {
111+
const versions = (await fs.readdir(param)).filter((v) => v !== '.DS_Store')
112+
const sortedVersions = semver.sort(versions);
113+
114+
const latestVersion = sortedVersions.at(-1);
115+
location = `${param}/${latestVersion}/libexec/openjdk.jdk/Contents/Home`
61116
}
117+
}
62118

63-
await codifySpawn(`jenv add ${location}`)
119+
try {
120+
await codifySpawn(`jenv add ${location}`, { throws: true });
121+
} catch (error: Error) {
122+
if (error.message.includes('jenv: cannot rehash')) {
123+
await this.rehash();
124+
return;
125+
}
64126

65-
return;
127+
throw error;
66128
}
67-
68-
await codifySpawn(`jenv add ${param}`);
69129
}
70130

71131
override async removeItem(param: string): Promise<void> {
72132
const isHomebrewInstalled = await Utils.isHomebrewInstalled();
73133

74-
if (JAVA_VERSION_INTEGER.test(param) && isHomebrewInstalled) {
75-
const parsedVersion = Number.parseInt(param, 10);
76-
const openjdkName = (parsedVersion === 22) ? 'openjdk' : `openjdk@${param}`;
77-
134+
if (isHomebrewInstalled && param.startsWith('/opt/homebrew/Cellar/openjdk@')) {
135+
const versionStr = param.split('/').at(4)?.split('@').at(1);
136+
if (!versionStr) {
137+
throw new Error(`jenv: malformed version str: ${versionStr}`)
138+
}
139+
140+
const parsedVersion = Number.parseInt(versionStr, 10)
141+
const openjdkName = (parsedVersion === 22) ? 'openjdk' : `openjdk@${parsedVersion}`;
142+
78143
const location = await this.getHomebrewInstallLocation(openjdkName);
79144
if (location) {
80145
await codifySpawn(`jenv remove ${location}`)
81146
await codifySpawn(`brew uninstall ${openjdkName}`)
82147
}
83-
148+
84149
return
85150
}
86151

87-
await codifySpawn(`jenv uninstall ${param}`);
152+
await codifySpawn(`jenv remove ${param}`);
88153
}
89154

90-
private async getHomebrewInstallLocation(openjdkName: string): Promise<string | null> {
155+
private async getHomebrewInstallLocation(openjdkName: string): Promise<null | string> {
91156
const { data: installInfo } = await codifySpawn(`brew list --formula -1 ${openjdkName}`)
92157

93158
// Example: /opt/homebrew/Cellar/openjdk@17/17.0.11/libexec/
@@ -104,7 +169,17 @@ export class JenvAddParameter extends ArrayStatefulParameter<JenvConfig, string>
104169
return libexec + 'openjdk.jdk/Contents/Home';
105170
}
106171

107-
private getFirstRegexGroup(regexp: RegExp, str: string): null | string {
108-
return Array.from(str.matchAll(regexp), m => m[1])?.at(0) ?? null;
172+
private async rehash(): Promise<void> {
173+
const { data: output } = await codifySpawn('jenv rehash', { throws: false })
174+
175+
if (output.includes('jenv: cannot rehash')) {
176+
const existingShims = output.match(/jenv: cannot rehash: (.*) exists/)?.at(1);
177+
if (!existingShims) {
178+
return;
179+
}
180+
181+
await fs.rename(existingShims, `${existingShims}-${nanoid(4)}`);
182+
await codifySpawn('jenv rehash', { throws: true })
183+
}
109184
}
110185
}

src/resources/java/jenv/jenv.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ export class JenvResource extends Resource<JenvConfig> {
3333
override async validate(parameters: Partial<JenvConfig>): Promise<void> {
3434
if (parameters.add) {
3535
for (const version of parameters.add) {
36-
if (JAVA_VERSION_INTEGER.test(version)) {
37-
if (!OPENJDK_SUPPORTED_VERSIONS.includes(Number.parseInt(version, 10))) {
36+
if (version.startsWith('/opt/homebrew/Cellar/openjdk@')) {
37+
const versionStr = version.split('/').at(4)?.split('@').at(1);
38+
39+
if (!OPENJDK_SUPPORTED_VERSIONS.includes(Number.parseInt(versionStr!, 10))) {
3840
throw new Error(`Version must be one of [${OPENJDK_SUPPORTED_VERSIONS.join(', ')}]`)
3941
}
40-
42+
4143
continue;
4244
}
4345

src/resources/scripting/file.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ export class FileResource extends Resource<FileConfig> {
1919
schema,
2020
parameterSettings: {
2121
path: { type: 'directory' },
22-
contents: { canModify: true },
23-
onlyCreate: { type: 'boolean' }
22+
contents: { canModify: true }
2423
},
2524
importAndDestroy:{
25+
refreshKeys: ['path', 'contents'],
2626
requiredParameters: ['path']
2727
}
2828
}

0 commit comments

Comments
 (0)