Skip to content

Commit b62dc62

Browse files
committed
[CODE-102] Add jenv resource
1 parent 8a8b61c commit b62dc62

9 files changed

Lines changed: 299 additions & 4 deletions

File tree

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { AwsProfileResource } from './resources/aws-cli/profile/aws-profile.js';
55
import { GitCloneResource } from './resources/git/clone/git-clone.js';
66
import { GitLfsResource } from './resources/git/lfs/git-lfs.js';
77
import { HomebrewResource } from './resources/homebrew/homebrew.js';
8+
import { JenvResource } from './resources/java/jenv/jenv.js';
89
import { NvmResource } from './resources/node/nvm/nvm.js';
910
import { PgcliResource } from './resources/pgcli/pgcli.js';
1011
import { PyenvResource } from './resources/python/pyenv/pyenv.js';
@@ -27,6 +28,7 @@ runPlugin(Plugin.create(
2728
new AwsProfileResource(),
2829
new TerraformResource(),
2930
new NvmResource(),
31+
new JenvResource(),
3032
new PgcliResource(),
3133
new VscodeResource(),
3234
new GitCloneResource(),
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { SpawnStatus, StatefulParameter } from 'codify-plugin-lib';
2+
3+
import { codifySpawn } from '../../../utils/codify-spawn.js';
4+
import { JenvConfig } from './jenv.js';
5+
6+
export class JenvGlobalParameter extends StatefulParameter<JenvConfig, string>{
7+
8+
constructor() {
9+
super({
10+
// The current version number must be at least as specific as the desired one. Ex: 3.12.9 = 3.12 but 3 != 3.12
11+
isEqual: (desired: string, current: string) => current.includes(desired)
12+
});
13+
}
14+
15+
async refresh(): Promise<null | string> {
16+
const { data, status } = await codifySpawn('jenv global', { throws: false })
17+
18+
if (status === SpawnStatus.ERROR) {
19+
return null;
20+
}
21+
22+
return data;
23+
}
24+
25+
async applyAdd(valueToAdd: string): Promise<void> {
26+
await codifySpawn(`jenv global ${valueToAdd}`)
27+
}
28+
29+
async applyModify(newValue: string): Promise<void> {
30+
await codifySpawn(`jenv global ${newValue}`)
31+
}
32+
33+
async applyRemove(): Promise<void> {
34+
await codifySpawn('jenv global system')
35+
}
36+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { ArrayStatefulParameter } from 'codify-plugin-lib';
2+
3+
import { SpawnStatus, codifySpawn } from '../../../utils/codify-spawn.js';
4+
import { Utils } from '../../../utils/index.js';
5+
import { JenvConfig } from './jenv.js';
6+
7+
export const OPENJDK_SUPPORTED_VERSIONS = [8, 11, 17, 21, 22]
8+
export const JAVA_VERSION_INTEGER = /^\d+$/;
9+
10+
export class JenvAddParameter extends ArrayStatefulParameter<JenvConfig, string> {
11+
async refresh(desired: null | string[]): Promise<null | string[]> {
12+
const { data: jenvVersions } = await codifySpawn('jenv versions')
13+
14+
return desired
15+
?.filter((v) => jenvVersions.includes(v))
16+
?.filter(Boolean) ?? null;
17+
}
18+
19+
async applyAddItem(param: string): Promise<void> {
20+
21+
const isHomebrewInstalled = await Utils.isHomebrewInstalled();
22+
23+
// Add special handling if the user specified an integer version. We add special functionality to automatically
24+
// install java if a lts version is specified and homebrew is installed.
25+
if (JAVA_VERSION_INTEGER.test(param)) {
26+
if (!isHomebrewInstalled) {
27+
throw new Error('Homebrew not detected. Cannot automatically install java version. Jenv does not automatically install' +
28+
' 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')
29+
}
30+
31+
const parsedVersion = Number.parseInt(param, 10);
32+
if (!OPENJDK_SUPPORTED_VERSIONS.includes(parsedVersion)) {
33+
throw new Error(`Unsupported version of java specified. Only [${OPENJDK_SUPPORTED_VERSIONS.join(', ')}] is supported`)
34+
}
35+
36+
const openjdkName = (parsedVersion === 22) ? 'openjdk' : `openjdk@${param}`;
37+
const { status } = await codifySpawn(`brew list --formula -1 ${openjdkName}`, { throws: false });
38+
39+
// That version is not currently installed with homebrew. Let's install it
40+
if (status === SpawnStatus.ERROR) {
41+
console.log(`Homebrew detected. Attempting to install java version ${openjdkName} automatically using homebrew`)
42+
await codifySpawn(`brew install ${openjdkName}`)
43+
}
44+
45+
const location = await this.getHomebrewInstallLocation(openjdkName);
46+
if (!location) {
47+
throw new Error('Unable to determine location of jdk installed by homebrew. Please report this to the Codify team');
48+
}
49+
50+
await codifySpawn(`jenv add ${location}`)
51+
52+
return;
53+
}
54+
55+
await codifySpawn(`jenv add ${param}`);
56+
}
57+
58+
async applyRemoveItem(param: string): Promise<void> {
59+
const isHomebrewInstalled = await Utils.isHomebrewInstalled();
60+
61+
if (JAVA_VERSION_INTEGER.test(param) && isHomebrewInstalled) {
62+
const parsedVersion = Number.parseInt(param, 10);
63+
const openjdkName = (parsedVersion === 22) ? 'openjdk' : `openjdk@${param}`;
64+
65+
const location = await this.getHomebrewInstallLocation(openjdkName);
66+
if (location) {
67+
await codifySpawn(`jenv remove ${location}`)
68+
await codifySpawn(`brew uninstall ${openjdkName}`)
69+
}
70+
71+
return
72+
}
73+
74+
await codifySpawn(`jenv uninstall ${param}`);
75+
}
76+
77+
private async getHomebrewInstallLocation(openjdkName: string): Promise<string | null> {
78+
const { data: installInfo } = await codifySpawn(`brew list --formula -1 ${openjdkName}`)
79+
80+
// Example: /opt/homebrew/Cellar/openjdk@17/17.0.11/libexec/
81+
const libexec = installInfo
82+
.split(/\n/)
83+
.find((l) => l.includes('libexec'))
84+
?.split('openjdk.jdk/')
85+
?.at(0)
86+
87+
if (!libexec) {
88+
return null;
89+
}
90+
91+
return libexec + '/openjdk.jdk/Contents/Home';
92+
}
93+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://www.codifycli.com/nvm.json",
4+
"title": "Nvm resource",
5+
"type": "object",
6+
"properties": {
7+
"add": {
8+
"type": "array",
9+
"items": {
10+
"type": "string"
11+
}
12+
},
13+
"global": {
14+
"type": "string"
15+
}
16+
},
17+
"additionalProperties": false
18+
}

src/resources/java/jenv/jenv.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { Resource, SpawnStatus } from 'codify-plugin-lib';
2+
import { ResourceConfig } from 'codify-schemas';
3+
import * as fs from 'node:fs';
4+
5+
import { codifySpawn } from '../../../utils/codify-spawn.js';
6+
import { FileUtils } from '../../../utils/file-utils.js';
7+
import { JenvGlobalParameter } from './global-parameter.js';
8+
import {
9+
JAVA_VERSION_INTEGER,
10+
JenvAddParameter,
11+
OPENJDK_SUPPORTED_VERSIONS
12+
} from './java-versions-parameter.js';
13+
import Schema from './jenv-schema.json';
14+
15+
export interface JenvConfig extends ResourceConfig {
16+
add?: string[],
17+
global?: string,
18+
}
19+
20+
export class JenvResource extends Resource<JenvConfig> {
21+
constructor() {
22+
super({
23+
dependencies: ['homebrew'],
24+
parameterOptions: {
25+
add: { order: 1, statefulParameter: new JenvAddParameter() },
26+
global: { order: 2, statefulParameter: new JenvGlobalParameter() },
27+
},
28+
schema: Schema,
29+
type: 'jenv'
30+
});
31+
}
32+
33+
async customValidation(parameters: Partial<JenvConfig>): Promise<void> {
34+
if (parameters.add) {
35+
for (const version of parameters.add) {
36+
if (JAVA_VERSION_INTEGER.test(version)) {
37+
if (!OPENJDK_SUPPORTED_VERSIONS.includes(Number.parseInt(version, 10))) {
38+
throw new Error(`Version must be one of [${OPENJDK_SUPPORTED_VERSIONS.join(', ')}]`)
39+
}
40+
41+
continue;
42+
}
43+
44+
if (!fs.existsSync(version)) {
45+
throw new Error(`Path does not exist. ${version} cannot be found on the file system`)
46+
}
47+
}
48+
}
49+
}
50+
51+
async refresh(): Promise<Partial<JenvConfig> | null> {
52+
const nvmQuery = await codifySpawn('which jenv', { throws: false })
53+
if (nvmQuery.status === SpawnStatus.ERROR) {
54+
return null
55+
}
56+
57+
return {};
58+
}
59+
60+
async applyCreate(): Promise<void> {
61+
await codifySpawn('git clone https://github.com/jenv/jenv.git ~/.jenv')
62+
63+
await FileUtils.addToStartupFile('export PATH="$HOME/.jenv/bin:$PATH"')
64+
await FileUtils.addToStartupFile('eval "$(jenv init -)"')
65+
66+
await codifySpawn('cat $HOME/.zshrc')
67+
await codifySpawn('jenv enable-plugin export')
68+
}
69+
70+
async applyDestroy(): Promise<void> {
71+
await codifySpawn('rm -rf $HOME/.jenv');
72+
73+
await FileUtils.removeLineFromZshrc('export PATH="$HOME/.jenv/bin:$PATH"')
74+
await FileUtils.removeLineFromZshrc('eval "$(jenv init -)"')
75+
}
76+
}

src/resources/terraform/terraform.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { StringIndexedObject } from 'codify-schemas';
33
import semver from 'semver';
44

55
import { codifySpawn } from '../../utils/codify-spawn.js';
6+
import { FileUtils } from '../../utils/file-utils.js';
67
import { Utils } from '../../utils/index.js';
78
import { untildify } from '../../utils/untildify.js';
89
import Schema from './terraform-schema.json';
910
import { HashicorpReleaseInfo, HashicorpReleasesAPIResponse, TerraformVersionInfo } from './terraform-types.js';
10-
import { FileUtils } from '../../utils/file-utils.js';
1111

1212
const TERRAFORM_RELEASES_API_URL = 'https://api.releases.hashicorp.com/v1/releases/terraform';
1313
const TERRAFORM_RELEASE_INFO_API_URL = (version: string) => `https://api.releases.hashicorp.com/v1/releases/terraform/${version}`;

src/resources/vscode/vscode.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { CreatePlan, DestroyPlan, Resource } from 'codify-plugin-lib';
22
import { ResourceConfig } from 'codify-schemas';
33
import path from 'node:path';
44

5-
import { codifySpawn, SpawnStatus } from '../../utils/codify-spawn.js';
5+
import { SpawnStatus, codifySpawn } from '../../utils/codify-spawn.js';
66
import Schema from './vscode-schema.json';
77

88
const VSCODE_APPLICATION_NAME = 'Visual Studio Code.app';
@@ -15,7 +15,6 @@ export interface VscodeConfig extends ResourceConfig {
1515
export class VscodeResource extends Resource<VscodeConfig> {
1616
constructor() {
1717
super({
18-
dependencies: ['homebrew'],
1918
parameterOptions: {
2019
directory: { default: '/Applications' }
2120
},

src/utils/file-utils.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as fsSync from 'node:fs';
22
import * as fs from 'node:fs/promises';
3-
import { homedir } from 'node:os';
3+
import os, { homedir } from 'node:os';
44
import path from 'node:path';
55

66
import { codifySpawn } from './codify-spawn.js';
@@ -14,6 +14,13 @@ export const FileUtils = {
1414
await codifySpawn(`echo "alias ${alias}=${escapedValue}" >> $HOME/.zshrc`)
1515
},
1616

17+
async addToStartupFile(line: string): Promise<void> {
18+
const lineToInsert = line.endsWith('\n') ? line : line + '\n';
19+
20+
await fs.appendFile(path.join(FileUtils.homeDir(), '.zshrc'), lineToInsert)
21+
},
22+
23+
1724
async addPathToZshrc(path: string, prepend: boolean): Promise<void> {
1825
const escapedPath = Utils.shellEscape(untildify(path))
1926

@@ -47,6 +54,10 @@ export const FileUtils = {
4754
}
4855
},
4956

57+
homeDir(): string {
58+
return os.homedir()
59+
},
60+
5061
async removeLineFromFile(filePath: string, search: RegExp | string): Promise<void> {
5162
const file = await fs.readFile(filePath, 'utf8')
5263
const lines = file.split('\n');

test/java/jenv/jenv.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { afterEach, beforeEach, describe, it } from 'vitest'
2+
import { PluginTester } from 'codify-plugin-test';
3+
import * as path from 'node:path';
4+
5+
describe('Jenv resource integration tests', () => {
6+
let plugin: PluginTester;
7+
8+
beforeEach(() => {
9+
plugin = new PluginTester(path.resolve('./src/index.ts'));
10+
})
11+
12+
it('Installs jenv and java with homebrew', { timeout: 500000 }, async () => {
13+
await plugin.fullTest([
14+
{ type: 'homebrew' },
15+
{
16+
type: 'jenv',
17+
global: '17',
18+
add: ['17']
19+
}
20+
]);
21+
});
22+
23+
it ('Can install additional java versions', { timeout: 500000 }, async () => {
24+
await plugin.fullTest([
25+
{ type: 'homebrew' },
26+
{
27+
type: 'jenv',
28+
global: '21',
29+
add: ['17', '21']
30+
}
31+
])
32+
})
33+
34+
it ('Can install additional java versions', { timeout: 500000 }, async () => {
35+
console.log('hihi')
36+
await plugin.fullTest([
37+
{ type: 'homebrew' },
38+
{
39+
type: 'jenv',
40+
global: '21',
41+
add: ['17', '21']
42+
}
43+
])
44+
})
45+
46+
it ('Can uninstall jenv', { timeout: 30000 }, async () => {
47+
await plugin.uninstall([
48+
{
49+
type: 'jenv',
50+
global: '21',
51+
add: ['17', '21']
52+
},
53+
{ type: 'homebrew' }
54+
])
55+
})
56+
57+
afterEach(() => {
58+
plugin.kill();
59+
})
60+
})

0 commit comments

Comments
 (0)