Skip to content

Commit f1814d7

Browse files
authored
Added coursier installation support for mac based environments (#38)
OpenJDK java does not appear on mac based system https://get-coursier.io/docs/cli-java#managed-jvms in managed jvm. Also the installation URL are different for Mac https://get-coursier.io/docs/cli-installation#native-launcher Here depending on the OS architecture we decide which coursier url and java version to pick. For test we use `proxyquire` as dependency to mock os environment. Updated yarn lock file and changed the rule prefixes from `node/` to `n/` to match the current naming convention of the eslint-plugin-n package (formerly eslint-plugin-node) Test: Tested the installation on mac and linux environments Also checked test runs there
1 parent fc7f454 commit f1814d7

File tree

7 files changed

+1452
-1037
lines changed

7 files changed

+1452
-1037
lines changed

.eslintrc.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
{
22
"extends": "./node_modules/gts/",
33
"rules": {
4-
"node/no-unpublished-import": 0,
5-
"node/no-extraneous-import": 0,
4+
"n/no-unpublished-import": 0,
5+
"n/no-unpublished-require": 0,
6+
"n/no-extraneous-import": 0,
7+
"n/no-process-exit": 0,
68
"eqeqeq": 0,
79
"@typescript-eslint/no-explicit-any": 0,
810
"@typescript-eslint/no-unused-vars": 0,
9-
"prefer-const": 0,
10-
"no-process-exit": 0
11+
"prefer-const": 0
1112
}
1213
}

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,18 @@
128128
"devDependencies": {
129129
"@types/mocha": "^10.0.6",
130130
"@types/node": "18.x",
131+
"@types/proxyquire": "^1.3.31",
131132
"@types/vscode": "^1.89.0",
132133
"@typescript-eslint/eslint-plugin": "^5.50.0",
133134
"@typescript-eslint/parser": "^7.0.2",
134135
"@vscode/test-cli": "^0.0.6",
135136
"@vscode/test-electron": "^2.3.9",
136137
"eslint": "^8.56.0",
138+
"eslint-plugin-n": "^17.16.1",
137139
"eslint-plugin-node": "^11.1.0",
138140
"eslint-plugin-prettier": "^5.0.0",
139141
"gts": "^5.2.0",
142+
"proxyquire": "^2.1.3",
140143
"sinon": "^15.1.0",
141144
"typescript": "4.9",
142145
"vsce": "^2.15.0"

src/server/install.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import * as os from 'os'
55
import * as axios from 'axios'
66
import * as vscode from 'vscode'
77
import {Inject} from '@nestjs/common'
8-
98
import {Utils} from '../utils/utils'
109
import {
1110
EXTENSION_CONTEXT_TOKEN,
@@ -21,7 +20,13 @@ export const INSTALL_BSP_COMMAND = 'bazelbsp.install'
2120

2221
const MAVEN_PACKAGE = 'org.jetbrains.bsp:bazel-bsp'
2322
const INSTALL_METHOD = 'org.jetbrains.bsp.bazel.install.Install'
24-
const COURSIER_URL = 'https://git.io/coursier-cli'
23+
const COURSIER_URL_DEFAULT = 'https://git.io/coursier-cli'
24+
const COURSIER_URL_APPLE_SILICON =
25+
'https://github.com/coursier/coursier/releases/latest/download/cs-aarch64-apple-darwin.gz'
26+
const COURSIER_URL_APPLE_INTEL =
27+
'https://github.com/coursier/launchers/raw/master/cs-x86_64-apple-darwin.gz'
28+
const OPEN_JDK_JAVA_17 = 'openjdk:1.17.0'
29+
const TEMURIN_JAVA_17 = 'temurin:1.17.0.0'
2530

2631
export interface InstallConfig {
2732
bazelProjectFilePath: string
@@ -121,13 +126,30 @@ export class BazelBSPInstaller {
121126
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'coursier-'))
122127
const coursierPath = path.join(tempDir, 'coursier')
123128

124-
this.outputChannel.appendLine(`Downloading Coursier from ${COURSIER_URL}`)
129+
let coursierUrl: string
130+
coursierUrl =
131+
os.platform() === 'darwin'
132+
? os.arch() === 'arm64'
133+
? COURSIER_URL_APPLE_SILICON
134+
: COURSIER_URL_APPLE_INTEL
135+
: COURSIER_URL_DEFAULT
136+
137+
this.outputChannel.appendLine(`Downloading Coursier from ${coursierUrl}`)
138+
125139
try {
126-
const response = await axios.default.get(COURSIER_URL, {
140+
const response = await axios.default.get(coursierUrl, {
127141
responseType: 'arraybuffer',
128142
})
129143

130-
await fs.writeFile(coursierPath, response.data)
144+
let fileData = response.data
145+
146+
// Decompress if downloading a gzipped file
147+
if (coursierUrl.endsWith('.gz')) {
148+
this.outputChannel.appendLine('Using gzipped Coursier')
149+
fileData = await Utils.gunzip(fileData)
150+
}
151+
152+
await fs.writeFile(coursierPath, fileData)
131153
await fs.chmod(coursierPath, 0o755)
132154
} catch (e) {
133155
this.outputChannel.appendLine(`Failed to download Coursier: ${e}`)
@@ -168,8 +190,10 @@ export class BazelBSPInstaller {
168190
.map(([key, value]) => `${key} "${value}"`)
169191
.join(' ')
170192

171-
// Full install command including flags.
172-
const installCommand = `"${coursierPath}" launch --jvm openjdk:1.17.0 ${MAVEN_PACKAGE}:${config.serverVersion} -M ${INSTALL_METHOD} -- ${flagsString}`
193+
const javaVersion =
194+
os.platform() === 'darwin' ? TEMURIN_JAVA_17 : OPEN_JDK_JAVA_17
195+
this.outputChannel.appendLine(`Using Java version: ${javaVersion}`)
196+
const installCommand = `"${coursierPath}" launch --jvm ${javaVersion} ${MAVEN_PACKAGE}:${config.serverVersion} -M ${INSTALL_METHOD} -- ${flagsString}`
173197

174198
// Report progress in output channel.
175199
const installProcess = cp.spawn(installCommand, {cwd: root, shell: true})

src/test/suite/install.test.ts

Lines changed: 114 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import * as bsp from '../../bsp/bsp'
88
import * as axios from 'axios'
99
import fs from 'fs/promises'
1010
import cp from 'child_process'
11+
import * as zlib from 'zlib'
12+
const proxyquire = require('proxyquire')
1113

1214
import {BazelBSPBuildClient} from '../../test-explorer/client'
1315
import {
@@ -29,34 +31,22 @@ suite('BSP Installer', () => {
2931
let sampleConn: MessageConnection
3032
let clientOutputChannel: vscode.LogOutputChannel
3133
let spawnStub: sinon.SinonStub
34+
let osMock: any
3235

3336
const sandbox = sinon.createSandbox()
3437

35-
beforeEach(async () => {
36-
let process = cp.spawn('echo')
37-
spawnStub = sandbox.stub(cp, 'spawn').returns(process)
38+
interface InstallTestConfig {
39+
platform: string
40+
arch: string
41+
isGzipped: boolean
42+
javaVersion: string
43+
}
3844

39-
// Return a fixed workspace root to avoid impact of local environment.
40-
sandbox.stub(Utils, 'getWorkspaceGitRoot').resolves('/repo/root')
45+
const setupInstallTest = (config: InstallTestConfig) => {
46+
// Set up OS mock values
47+
osMock.platform = () => config.platform
48+
osMock.arch = () => config.arch
4149

42-
// Set up the testing app which includes injected dependnecies and the stubbed BuildServerManager
43-
ctx = {subscriptions: []} as unknown as vscode.ExtensionContext
44-
const moduleRef = await Test.createTestingModule({
45-
providers: [
46-
outputChannelProvider,
47-
contextProviderFactory(ctx),
48-
BazelBSPInstaller,
49-
],
50-
}).compile()
51-
bazelBSPInstaller = moduleRef.get(BazelBSPInstaller)
52-
})
53-
54-
afterEach(() => {
55-
sandbox.restore()
56-
})
57-
58-
test('spawn install process', async () => {
59-
// User selection to install BSP
6050
sandbox
6151
.stub(vscode.window, 'showErrorMessage')
6252
.resolves({title: 'Install BSP'})
@@ -80,29 +70,113 @@ load("@rules_python//python:defs.bzl", "PyInfo", "PyRuntimeInfo")
8070
load("//aspects:utils/utils.bzl", "create_struct", "file_location", "to_file_location")`
8171
)
8272

83-
// Simulated data returned by coursier download request.
84-
const sampleData = 'sample data'
85-
sandbox.stub(axios.default, 'get').resolves({data: sampleData} as any)
73+
const originalData = 'sample data'
74+
const responseData = config.isGzipped
75+
? zlib.gzipSync(Buffer.from(originalData))
76+
: originalData
8677

87-
const writeFileSpy = sandbox.spy(fs, 'writeFile')
88-
const installResult = await bazelBSPInstaller.install()
78+
sandbox.stub(axios.default, 'get').resolves({
79+
data: responseData,
80+
} as any)
8981

90-
// Confirm that the coursier data was written to a file.
91-
const coursierPath = writeFileSpy.getCalls()[0].args[0]
92-
const writtenData = writeFileSpy.getCalls()[0].args[1]
93-
assert.equal(writtenData, sampleData)
94-
assert.equal(spawnStub.callCount, 1)
82+
return {originalData}
83+
}
9584

96-
// Just confirm that coursier path was part of the spawn call, to leave flexibility for other changes to the command.
97-
assert.ok(spawnStub.getCalls()[0].args[0].includes(coursierPath))
98-
assert.ok(spawnStub.getCalls()[0].args[0].includes('--jvm openjdk:1.17.0'))
99-
assert.ok(installResult)
85+
beforeEach(async () => {
86+
let process = cp.spawn('echo')
87+
spawnStub = sandbox.stub(cp, 'spawn').returns(process)
88+
sandbox.stub(Utils, 'getWorkspaceGitRoot').resolves('/repo/root')
89+
90+
osMock = {
91+
platform: () => 'darwin',
92+
arch: () => 'arm64',
93+
}
94+
95+
// Use proxyquire to inject the OS mock
96+
const BazelBSPInstallerProxy = proxyquire('../../server/install', {
97+
os: osMock,
98+
}).BazelBSPInstaller
99+
100+
ctx = {subscriptions: []} as unknown as vscode.ExtensionContext
101+
const moduleRef = await Test.createTestingModule({
102+
providers: [
103+
outputChannelProvider,
104+
contextProviderFactory(ctx),
105+
{
106+
provide: BazelBSPInstaller,
107+
useClass: BazelBSPInstallerProxy,
108+
},
109+
],
110+
}).compile()
111+
bazelBSPInstaller = moduleRef.get(BazelBSPInstaller)
112+
})
100113

101-
const updatedContents = writeFileSpy.getCalls()[1].args[1]
102-
const expectedContents = `load("@rules_python//python:defs.bzl", "PyInfo", "PyRuntimeInfo")
114+
afterEach(() => {
115+
sandbox.restore()
116+
})
117+
118+
const testConfigs: {[key: string]: InstallTestConfig} = {
119+
macArm64: {
120+
platform: 'darwin',
121+
arch: 'arm64',
122+
isGzipped: true,
123+
javaVersion: 'temurin:1.17.0.0',
124+
},
125+
macIntel: {
126+
platform: 'darwin',
127+
arch: 'x64',
128+
isGzipped: true,
129+
javaVersion: 'temurin:1.17.0.0',
130+
},
131+
linux: {
132+
platform: 'linux',
133+
arch: 'x64',
134+
isGzipped: false,
135+
javaVersion: 'openjdk:1.17.0',
136+
},
137+
}
138+
139+
Object.entries(testConfigs).forEach(([name, config]) => {
140+
test(`spawn install process - ${name}`, async () => {
141+
const {originalData} = setupInstallTest(config)
142+
143+
const writeFileSpy = sandbox.spy(fs, 'writeFile')
144+
const chmodSpy = sandbox.spy(fs, 'chmod')
145+
const installResult = await bazelBSPInstaller.install()
146+
147+
// Verify coursier download and permissions
148+
assert.equal(writeFileSpy.callCount, 2)
149+
const coursierPath = writeFileSpy.getCalls()[0].args[0]
150+
const writtenData = writeFileSpy.getCalls()[0].args[1]
151+
152+
if (config.isGzipped) {
153+
assert.equal(writtenData.toString(), originalData)
154+
} else {
155+
assert.equal(writtenData, originalData)
156+
}
157+
158+
assert.equal(chmodSpy.callCount, 1)
159+
assert.equal(chmodSpy.getCalls()[0].args[0], coursierPath)
160+
assert.equal(chmodSpy.getCalls()[0].args[1], 0o755)
161+
162+
// Verify spawn command
163+
const updatedContents = writeFileSpy.getCalls()[1].args[1]
164+
const expectedContents = `load("@rules_python//python:defs.bzl", "PyInfo", "PyRuntimeInfo")
103165
104166
load("//aspects:utils/utils.bzl", "create_struct", "file_location", "to_file_location")`
105-
assert.equal(updatedContents, expectedContents)
167+
168+
assert.equal(updatedContents, expectedContents)
169+
assert.equal(spawnStub.callCount, 1)
170+
const spawnCall = spawnStub.getCalls()[0]
171+
assert.ok(spawnCall.args[0].includes(coursierPath))
172+
assert.ok(spawnCall.args[0].includes(`--jvm ${config.javaVersion}`))
173+
assert.ok(spawnCall.args[0].includes('org.jetbrains.bsp:bazel-bsp:2.0.0'))
174+
assert.deepStrictEqual(spawnCall.args[1], {
175+
cwd: '/repo/root',
176+
shell: true,
177+
})
178+
assert.ok(installResult)
179+
})
106180
})
107181

108182
test('failed coursier download', async () => {

src/test/suite/utils.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as vscode from 'vscode'
22
import * as assert from 'assert'
33
import sinon from 'sinon'
44
import {afterEach} from 'mocha'
5+
import * as zlib from 'zlib'
56

67
import {Deferred, Utils} from '../../utils/utils'
78

@@ -99,6 +100,25 @@ suite('Utils Test Suite', () => {
99100
)
100101
})
101102
})
103+
104+
test('gunzip successful decompression', async () => {
105+
const originalData = 'Hello, World!'
106+
const compressedData = zlib.gzipSync(Buffer.from(originalData))
107+
108+
const result = await Utils.gunzip(compressedData)
109+
assert.strictEqual(result.toString(), originalData)
110+
})
111+
112+
test('gunzip handles invalid data', async () => {
113+
const invalidData = Buffer.from('not compressed data')
114+
115+
try {
116+
await Utils.gunzip(invalidData)
117+
assert.fail('Should have thrown an error')
118+
} catch (error) {
119+
assert.ok(error instanceof Error)
120+
}
121+
})
102122
})
103123

104124
suite('Deferred Promise Test Suite', () => {

src/utils/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as path from 'path'
33
import {promisify} from 'util'
44
import {exec} from 'child_process'
55
import * as fs from 'fs/promises'
6+
import * as zlib from 'zlib'
67

78
const execAsync = promisify(exec)
89

@@ -55,6 +56,15 @@ export class Utils {
5556
return Utils.getGitRootFromPath(workspaceRoot.fsPath)
5657
}
5758

59+
static async gunzip(data: Buffer): Promise<Buffer> {
60+
return new Promise((resolve, reject) => {
61+
zlib.gunzip(data, (err, result) => {
62+
if (err) reject(err)
63+
else resolve(result)
64+
})
65+
})
66+
}
67+
5868
// Use wrapped file i/o operations for use in stubbing.
5969
static async readdir(path: string): Promise<string[]> {
6070
return fs.readdir(path)

0 commit comments

Comments
 (0)