Skip to content

Commit d8820c8

Browse files
committed
feat: Fixes jenv for linux
1 parent 346cdc2 commit d8820c8

5 files changed

Lines changed: 209 additions & 46 deletions

File tree

scripts/init.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
tart clone ghcr.io/cirruslabs/macos-tahoe-base:latest codify-test-vm
22
tart set codify-test-vm --memory 6124 --cpu 4
33

4+
tart clone ghcr.io/cirruslabs/ubuntu:latest codify-test-vm-linux
5+
tart set codify-test-vm-linux --memory 6124 --cpu 4
6+
7+
48
# tart clone ghcr.io/kevinwang5658/sonoma-codify:v0.0.3 codify-sonoma

scripts/run-tests.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ async function launchPersistentTest(test: string, debug: boolean, operatingSyste
9898

9999
console.log('Done refreshing files on VM. Starting tests...');
100100
VerbosityLevel.set(3);
101-
await codifySpawn(`tart exec ${vmName} ${shell} -c ${operatingSystem === 'darwin' ? '-i' : ''} "cd ${dir} && FORCE_COLOR=true npm run test -- ${test} --disable-console-intercept ${debugFlag} --no-file-parallelism"`, { throws: false });
101+
await codifySpawn(`tart exec -i ${vmName} ${shell} -c -i "cd ${dir} && FORCE_COLOR=true npm run test -- ${test} --disable-console-intercept ${debugFlag} --no-file-parallelism"`, { throws: false });
102102
// }
103103
}
104104

@@ -125,6 +125,10 @@ async function launchPersistentVm(operatingSystem: string) {
125125
await testSpawn(`sshpass -p "admin" rsync -avz -e 'ssh -o PubkeyAuthentication=no -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' --exclude 'node_modules' --exclude '.git' --exclude 'dist' --exclude '.fleet' ${process.cwd()} admin@${ipAddr}:~`);
126126
if (operatingSystem === 'darwin') {
127127
await testSpawn(`tart exec ${newVmName} ${shell} -i -c "mv ~/.zprofile ~/.zshenv"`);
128+
} else {
129+
await testSpawn(`tart exec ${newVmName} ${shell} -i -c "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash"`)
130+
await testSpawn(`tart exec ${newVmName} ${shell} -i -c "nvm install 24; nvm alias default 24"`)
131+
await testSpawn(`tart exec ${newVmName} ${shell} -i -c "echo 'export XDG_RUNTIME_DIR=/run/user/$(id -u)' >> ~/.bashrc"`)
128132
}
129133

130134
await testSpawn(`tart exec ${newVmName} ${shell} -i -c "cd ~/codify-homebrew-plugin && npm ci"`);

src/resources/java/jenv/java-versions-parameter.ts

Lines changed: 97 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,67 @@ import { Utils } from '../../../utils/index.js';
77
import { JenvConfig } from './jenv.js';
88
import { nanoid } from 'nanoid';
99

10-
export const OPENJDK_SUPPORTED_VERSIONS = [8, 11, 17, 21, 22]
1110
export const JAVA_VERSION_INTEGER = /^\d+$/;
1211

12+
// Maps an integer version to the Homebrew Cellar prefix (macOS)
13+
function toBrewPath(version: number): string {
14+
return `/opt/homebrew/Cellar/openjdk@${version}`;
15+
}
16+
17+
// Maps an integer version to the system JVM path (Linux)
18+
// Covers both x86_64 (amd64) and aarch64 (arm64) architectures
19+
async function toLinuxJvmPath(version: number): Promise<string> {
20+
const candidates = [
21+
`/usr/lib/jvm/java-${version}-openjdk-amd64`,
22+
`/usr/lib/jvm/java-${version}-openjdk-arm64`,
23+
`/usr/lib/jvm/java-${version}-openjdk`,
24+
`/usr/lib/jvm/temurin-${version}`,
25+
`/usr/lib/jvm/java-${version}`,
26+
];
27+
28+
for (const candidate of candidates) {
29+
if (await FileUtils.exists(candidate)) {
30+
return candidate;
31+
}
32+
}
33+
34+
// Return the most common path as the default even if it doesn't exist yet
35+
const isArm = await Utils.isArmArch();
36+
return `/usr/lib/jvm/java-${version}-openjdk-${isArm ? 'arm64' : 'amd64'}`;
37+
}
38+
39+
function parseVersionFromBrewPath(p: string): string | undefined {
40+
return p.split('/').at(4)?.split('@').at(1);
41+
}
42+
1343
export class JenvAddParameter extends ArrayStatefulParameter<JenvConfig, string> {
1444
getSettings(): ArrayParameterSetting {
1545
return {
1646
type: 'array',
1747
itemType: 'directory',
1848
isElementEqual: (a, b) => b.includes(a),
1949
transformation: {
20-
to: (input: string[]) =>
21-
input.map((i) => {
22-
if (OPENJDK_SUPPORTED_VERSIONS.includes(Number.parseInt(i, 10))) {
23-
return `/opt/homebrew/Cellar/openjdk@${Number.parseInt(i, 10)}`
50+
to: async (input: string[]) =>
51+
Promise.all(input.map(async (i) => {
52+
const parsed = Number.parseInt(i, 10);
53+
if (JAVA_VERSION_INTEGER.test(i) && !Number.isNaN(parsed)) {
54+
return Utils.isMacOS()
55+
? toBrewPath(parsed)
56+
: await toLinuxJvmPath(parsed);
2457
}
2558

2659
return i;
27-
}),
60+
})),
2861
// De-dupe the results for imports.
2962
from: (output: string[]) => [...new Set(output.map((i) => {
3063
if (i.startsWith('/opt/homebrew/Cellar/openjdk@')) {
31-
return i.split('/').at(4)?.split('@').at(1)
64+
return parseVersionFromBrewPath(i);
65+
}
66+
67+
// Linux: /usr/lib/jvm/java-17-openjdk-amd64 → "17"
68+
const linuxMatch = i.match(/\/usr\/lib\/jvm\/(?:java-|temurin-)(\d+)/);
69+
if (linuxMatch) {
70+
return linuxMatch[1];
3271
}
3372

3473
return i;
@@ -41,12 +80,12 @@ export class JenvAddParameter extends ArrayStatefulParameter<JenvConfig, string>
4180
const $ = getPty();
4281

4382
const { data: jenvRoot } = await $.spawn('jenv root')
44-
const versions = (await fs.readdir(`${jenvRoot}/versions`)).filter((v) => v !== '.DS_store');
83+
const versions = (await fs.readdir(`${jenvRoot.trim()}/versions`)).filter((v) => v !== '.DS_store' && v !== '.DS_Store');
4584

4685
// 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
4786
const versionPaths = new Set(
4887
await Promise.all(versions.map((v) =>
49-
fs.readlink(`${jenvRoot}/versions/${v}`)
88+
fs.readlink(`${jenvRoot.trim()}/versions/${v}`)
5089
))
5190
)
5291

@@ -58,42 +97,47 @@ export class JenvAddParameter extends ArrayStatefulParameter<JenvConfig, string>
5897
// Re-map the path back to what was provided in the config
5998
.map((v) => {
6099
const matched = params?.find((p) => v.includes(p));
61-
return matched === undefined
62-
? v
63-
: matched;
100+
return matched === undefined ? v : matched;
64101
})
65102
.filter((v) => {
66-
const versionStr = v.split('/').at(4)!.split('@').at(1)!;
67-
return installedVersions.includes(versionStr);
103+
// macOS Homebrew path: /opt/homebrew/Cellar/openjdk@17/...
104+
if (v.startsWith('/opt/homebrew/Cellar/openjdk@')) {
105+
const versionStr = parseVersionFromBrewPath(v);
106+
return versionStr !== undefined && installedVersions.some((iv) => iv.startsWith(versionStr));
107+
}
108+
109+
// Linux JVM path: /usr/lib/jvm/java-17-openjdk-amd64
110+
const linuxMatch = v.match(/\/usr\/lib\/jvm\/(?:java-|temurin-)(\d+)/);
111+
if (linuxMatch) {
112+
const versionStr = linuxMatch[1];
113+
return installedVersions.some((iv) => iv.startsWith(versionStr));
114+
}
115+
116+
// Generic path: match against installed version strings
117+
return installedVersions.some((iv) => v.includes(iv));
68118
});
69119
}
70120

71121
override async addItem(param: string): Promise<void> {
72122
let location = param;
73123

74-
// Check if we should auto install it from homebrew first
124+
// macOS: auto-install from Homebrew
75125
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
78126
if (!(await FileUtils.exists(param))) {
79127
const isHomebrewInstalled = await Utils.isHomebrewInstalled();
80128
if (!isHomebrewInstalled) {
81129
throw new Error('Homebrew not detected. Cannot automatically install java version. Jenv does not automatically install' +
82130
' 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')
83131
}
84132

85-
const versionStr = param.split('/').at(4)?.split('@').at(1);
133+
const versionStr = parseVersionFromBrewPath(param);
86134
if (!versionStr) {
87135
throw new Error(`jenv: malformed version str: ${versionStr}`)
88136
}
89137

90138
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-
95139
const $ = getPty();
96-
const openjdkName = (parsedVersion === 22) ? 'openjdk' : `openjdk@${parsedVersion}`;
140+
const openjdkName = `openjdk@${parsedVersion}`;
97141
const { status } = await $.spawnSafe(`brew list --formula -1 ${openjdkName}`, { interactive: true });
98142

99143
// That version is not currently installed with homebrew. Let's install it
@@ -107,7 +151,7 @@ export class JenvAddParameter extends ArrayStatefulParameter<JenvConfig, string>
107151
throw new Error('Unable to determine location of jdk installed by homebrew. Please report this to the Codify team');
108152
}
109153

110-
// Already exists on the file system let's re-map to the actual path
154+
// Already exists on the file system: re-map to the actual versioned path
111155
} else if (!param.endsWith('libexec/openjdk.jdk/Contents/Home')) {
112156
const versions = (await fs.readdir(param)).filter((v) => v !== '.DS_Store')
113157
const sortedVersions = semver.sort(versions);
@@ -117,6 +161,24 @@ export class JenvAddParameter extends ArrayStatefulParameter<JenvConfig, string>
117161
}
118162
}
119163

164+
// Linux: auto-install via apt
165+
if (Utils.isLinux()) {
166+
const linuxMatch = param.match(/\/usr\/lib\/jvm\/(?:java-|temurin-)(\d+)/);
167+
if (linuxMatch && !(await FileUtils.exists(param))) {
168+
const version = linuxMatch[1];
169+
const $ = getPty();
170+
const packageName = `openjdk-${version}-jdk`;
171+
const { status } = await $.spawnSafe(`dpkg -s ${packageName}`, { interactive: true, requiresRoot: true });
172+
173+
if (status === SpawnStatus.ERROR) {
174+
console.log(`apt detected. Attempting to install java version ${packageName} automatically`)
175+
await $.spawn(`apt-get install -y ${packageName}`, { interactive: true, requiresRoot: true })
176+
}
177+
178+
location = await toLinuxJvmPath(Number.parseInt(version, 10));
179+
}
180+
}
181+
120182
const $ = getPty();
121183
try {
122184
await $.spawn(`jenv add ${location}`, { interactive: true });
@@ -135,7 +197,7 @@ export class JenvAddParameter extends ArrayStatefulParameter<JenvConfig, string>
135197
const isHomebrewInstalled = await Utils.isHomebrewInstalled();
136198

137199
if (isHomebrewInstalled && param.startsWith('/opt/homebrew/Cellar/openjdk@')) {
138-
const versionStr = param.split('/').at(4)?.split('@').at(1);
200+
const versionStr = parseVersionFromBrewPath(param);
139201
if (!versionStr) {
140202
throw new Error(`jenv: malformed version str: ${versionStr}`)
141203
}
@@ -152,6 +214,16 @@ export class JenvAddParameter extends ArrayStatefulParameter<JenvConfig, string>
152214
return
153215
}
154216

217+
if (Utils.isLinux()) {
218+
const linuxMatch = param.match(/\/usr\/lib\/jvm\/(?:java-|temurin-)(\d+)/);
219+
if (linuxMatch) {
220+
const version = linuxMatch[1];
221+
await $.spawn(`jenv remove ${param}`, { interactive: true })
222+
await $.spawn(`sudo apt-get remove -y openjdk-${version}-jdk`, { interactive: true })
223+
return;
224+
}
225+
}
226+
155227
await $.spawn(`jenv remove ${param}`, { interactive: true });
156228
}
157229

src/resources/java/jenv/jenv.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import { OS, ResourceConfig } from '@codifycli/schemas';
33
import * as fs from 'node:fs';
44

55
import { FileUtils } from '../../../utils/file-utils.js';
6+
import { Utils } from '../../../utils/index.js';
67
import { JenvGlobalParameter } from './global-parameter.js';
78
import {
89
JenvAddParameter,
9-
OPENJDK_SUPPORTED_VERSIONS
10+
JAVA_VERSION_INTEGER,
1011
} from './java-versions-parameter.js';
1112
import Schema from './jenv-schema.json';
1213

@@ -19,9 +20,9 @@ export class JenvResource extends Resource<JenvConfig> {
1920
getSettings(): ResourceSettings<JenvConfig> {
2021
return {
2122
id: 'jenv',
22-
operatingSystems: [OS.Darwin],
23+
operatingSystems: [OS.Darwin, OS.Linux],
2324
schema: Schema,
24-
dependencies: ['homebrew'],
25+
dependencies: Utils.isMacOS() ? ['homebrew'] : [],
2526
parameterSettings: {
2627
add: { type: 'stateful', definition: new JenvAddParameter(), order: 1 },
2728
global: { type: 'stateful', definition: new JenvGlobalParameter(), order: 2 },
@@ -33,18 +34,16 @@ export class JenvResource extends Resource<JenvConfig> {
3334
if (parameters.add) {
3435
for (const version of parameters.add) {
3536
if (version.startsWith('/opt/homebrew/Cellar/openjdk@')) {
36-
const versionStr = version.split('/').at(4)?.split('@').at(1);
37-
38-
if (!OPENJDK_SUPPORTED_VERSIONS.includes(Number.parseInt(versionStr!, 10))) {
39-
throw new Error(`Version must be one of [${OPENJDK_SUPPORTED_VERSIONS.join(', ')}]`)
40-
}
41-
4237
continue;
4338
}
4439

45-
if (!fs.existsSync(version)) {
46-
throw new Error(`Path does not exist. ${version} cannot be found on the file system`)
40+
if (JAVA_VERSION_INTEGER.test(version)) {
41+
continue;
4742
}
43+
44+
// if (!fs.existsSync(version)) {
45+
// throw new Error(`Path does not exist. ${version} cannot be found on the file system`)
46+
// }
4847
}
4948
}
5049
}
@@ -69,15 +68,26 @@ export class JenvResource extends Resource<JenvConfig> {
6968

7069
override async create(): Promise<void> {
7170
const $ = getPty();
72-
await this.assertBrewInstalled()
7371

74-
const jenvQuery = await $.spawnSafe('which jenv', { interactive: true })
75-
if (jenvQuery.status === SpawnStatus.ERROR) {
76-
await $.spawn('brew install jenv', { interactive: true })
72+
if (Utils.isMacOS()) {
73+
await this.assertBrewInstalled()
74+
75+
const jenvQuery = await $.spawnSafe('which jenv', { interactive: true })
76+
if (jenvQuery.status === SpawnStatus.ERROR) {
77+
await $.spawn('brew install jenv', { interactive: true })
78+
}
79+
} else {
80+
const jenvQuery = await $.spawnSafe('which jenv', { interactive: true })
81+
if (jenvQuery.status === SpawnStatus.ERROR) {
82+
const result = await $.spawnSafe('git clone https://github.com/jenv/jenv.git ~/.jenv', { interactive: true })
83+
if (result.status === SpawnStatus.ERROR && !result.data.includes('already exists and is not an empty directory.')) {
84+
throw new Error(result.data);
85+
}
86+
}
7787
}
7888

79-
const jenvDoctor = await $.spawn('jenv doctor', { interactive: true })
80-
if (jenvDoctor.data.includes('Jenv is not loaded in')) {
89+
const jenvDoctor = await $.spawnSafe('jenv doctor', { interactive: true })
90+
if (jenvDoctor.data.includes('Jenv is not loaded in') || jenvDoctor.status === SpawnStatus.ERROR) {
8191
await FileUtils.addToStartupFile('export PATH="$HOME/.jenv/bin:$PATH"')
8292
await FileUtils.addToStartupFile('eval "$(jenv init -)"')
8393

0 commit comments

Comments
 (0)