Skip to content

Commit 9ebc136

Browse files
Llandy3dcristianoventuragoing-confetti
authored
feat: linux support (#513)
Co-authored-by: Cristiano Ventura <mrk.cristiano@gmail.com> Co-authored-by: Uladzimir Dzmitračkoŭ <3773351+going-confetti@users.noreply.github.com>
1 parent fdfce94 commit 9ebc136

File tree

9 files changed

+143
-33
lines changed

9 files changed

+143
-33
lines changed

.github/workflows/release.yml

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ jobs:
1010
strategy:
1111
fail-fast: false
1212
matrix:
13-
# linux disabled until we get to work to support it
14-
#platform: [macos-latest, ubuntu-20.04, windows-latest]
15-
platform: [macos-latest, windows-latest]
13+
# There is a bug on Ubuntu 22.04 (ubuntu-latest) regarding stripping a binary for a different
14+
# architecture. Since we include both x86_64 and arm64 in linux a solution is to use ubuntu 20.04
15+
# https://github.com/electron/forge/issues/3102
16+
# https://github.com/electron/forge/issues/3701
17+
platform: [macos-latest, windows-latest, ubuntu-20.04]
1618

1719
runs-on: ${{ matrix.platform }}
1820
steps:
@@ -91,6 +93,22 @@ jobs:
9193
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
9294
run: npm run publish
9395

96+
- name: publish Linux
97+
if: startsWith(matrix.platform, 'ubuntu-')
98+
env:
99+
NODE_OPTIONS: '--max_old_space_size=8192'
100+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
101+
# sentry integration
102+
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
103+
# sentry vite plugin integration during build
104+
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
105+
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
106+
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
107+
run: |
108+
sudo apt install -y rpm
109+
npm run publish
110+
npm run publish -- --arch=arm64
111+
94112
- name: cleanup macos certificates
95113
if: startsWith(matrix.platform, 'macos-')
96114
run: |

forge.config.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ function getPlatformSpecificResources() {
1616
// Otherwise the x86_64 build will still be having the resources/arm64 only binaries
1717
if (getPlatform() === 'mac') {
1818
return ['./resources/mac/arm64', './resources/mac/x86_64']
19+
} else if (getPlatform() === 'linux') {
20+
return ['./resources/linux/arm64', './resources/linux/x86_64']
1921
}
2022

2123
return [path.join('./resources/', getPlatform(), getArch())]
2224
}
2325

2426
const config: ForgeConfig = {
2527
packagerConfig: {
28+
name: 'Grafana k6 Studio',
29+
executableName: 'k6-studio',
2630
icon: './resources/icons/logo',
2731
asar: true,
2832
extraResource: [
@@ -64,9 +68,8 @@ const config: ForgeConfig = {
6468
},
6569
['darwin']
6670
),
67-
new MakerRpm({}),
68-
new MakerRpm({}),
69-
new MakerDeb({ options: { icon: './src/assets/icons/logo.png' } }),
71+
new MakerRpm({ options: { icon: './resources/icons/logo.png' } }),
72+
new MakerDeb({ options: { icon: './resources/icons/logo.png' } }),
7073
],
7174
plugins: [
7275
new VitePlugin({

install-k6.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const K6_VERSION = 'v0.55.0'
55
const K6_PATH_MAC_AMD = `k6-${K6_VERSION}-macos-amd64`
66
const K6_PATH_MAC_ARM = `k6-${K6_VERSION}-macos-arm64`
77
const K6_PATH_WIN_AMD = `k6-${K6_VERSION}-windows-amd64`
8+
const K6_PATH_LINUX_AMD = `k6-${K6_VERSION}-linux-amd64`
9+
const K6_PATH_LINUX_ARM = `k6-${K6_VERSION}-linux-arm64`
810

911
const getMacOSK6Binary = () => {
1012
const command = `
@@ -52,6 +54,32 @@ Remove-Item -Path "${K6_PATH_WIN_AMD}" -Recurse
5254
execSync(command, { shell: 'powershell.exe' })
5355
}
5456

57+
const getLinuxK6Binary = () => {
58+
const command = `
59+
# download binaries
60+
curl -LO https://github.com/grafana/k6/releases/download/${K6_VERSION}/${K6_PATH_LINUX_AMD}.tar.gz
61+
curl -LO https://github.com/grafana/k6/releases/download/${K6_VERSION}/${K6_PATH_LINUX_ARM}.tar.gz
62+
63+
# unzip & smoke test
64+
tar -zxf ${K6_PATH_LINUX_AMD}.tar.gz
65+
tar -zxf ${K6_PATH_LINUX_ARM}.tar.gz
66+
${K6_PATH_LINUX_AMD}/k6 version
67+
# ${K6_PATH_LINUX_ARM}/k6 version ## if we move to separate images for architectures we could test this one as well
68+
69+
# move to resource folder
70+
mv ${K6_PATH_LINUX_AMD}/k6 resources/linux/x86_64
71+
mv ${K6_PATH_LINUX_ARM}/k6 resources/linux/arm64
72+
73+
# cleanup
74+
rm ${K6_PATH_LINUX_AMD}.tar.gz
75+
rm ${K6_PATH_LINUX_ARM}.tar.gz
76+
rmdir ${K6_PATH_LINUX_AMD}
77+
rmdir ${K6_PATH_LINUX_ARM}
78+
`
79+
80+
execSync(command)
81+
}
82+
5583
switch (process.platform) {
5684
case 'darwin':
5785
// we check only for one arch since we include both binaries
@@ -70,6 +98,15 @@ switch (process.platform) {
7098
console.log('k6 binary download completed')
7199
}
72100
break
101+
case 'linux':
102+
// we check only for one arch since we include both binaries
103+
if (!existsSync('resources/linux/x86_64/k6')) {
104+
console.log('k6 binary not found')
105+
console.log('downloading k6... this might take some time...')
106+
getLinuxK6Binary()
107+
console.log('k6 binary download completed')
108+
}
109+
break
73110
default:
74111
console.log(`unsupported platform found: ${process.platform}`)
75112
}
33.7 MB
Binary file not shown.

src/browser.ts

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,23 @@ import { mkdtemp } from 'fs/promises'
1010
import path from 'path'
1111
import os from 'os'
1212
import { appSettings } from './main'
13+
import { exec, spawn } from 'child_process'
14+
import { getPlatform } from './utils/electron'
15+
import log from 'electron-log/main'
1316

1417
const createUserDataDir = async () => {
1518
return mkdtemp(path.join(os.tmpdir(), 'k6-studio-'))
1619
}
1720

18-
export function getBrowserPath() {
21+
export async function getBrowserPath() {
1922
const { recorder } = appSettings
2023

2124
if (recorder.detectBrowserPath) {
25+
if (getPlatform() === 'linux' && process.arch === 'arm64') {
26+
// Chrome is not available for arm64, use Chromium instead
27+
return await getChromiumPath()
28+
}
29+
2230
return computeSystemExecutablePath({
2331
browser: Browser.CHROME,
2432
channel: ChromeReleaseChannel.STABLE,
@@ -32,7 +40,7 @@ export const launchBrowser = async (
3240
browserWindow: BrowserWindow,
3341
url?: string
3442
) => {
35-
const path = getBrowserPath()
43+
const path = await getBrowserPath()
3644
console.info(`browser path: ${path}`)
3745

3846
const userDataDir = await createUserDataDir()
@@ -54,24 +62,51 @@ export const launchBrowser = async (
5462
return Promise.resolve()
5563
}
5664

65+
const args = [
66+
'--new',
67+
'--args',
68+
`--user-data-dir=${userDataDir}`,
69+
'--hide-crash-restore-bubble',
70+
'--test-type',
71+
'--no-default-browser-check',
72+
'--no-first-run',
73+
'--disable-background-networking',
74+
'--disable-component-update',
75+
'--disable-search-engine-choice-screen',
76+
`--proxy-server=http://localhost:${appSettings.proxy.port}`,
77+
`--ignore-certificate-errors-spki-list=${certificateSPKI}`,
78+
disableChromeOptimizations,
79+
url?.trim() || 'about:blank',
80+
]
81+
82+
// if we are on linux we spawn the browser directly and attach the on exit callback
83+
if (getPlatform() === 'linux') {
84+
const browserProc = spawn(path, args)
85+
browserProc.once('exit', sendBrowserClosedEvent)
86+
return browserProc
87+
}
88+
89+
// macOS & windows
5790
return launch({
5891
executablePath: path,
59-
args: [
60-
'--new',
61-
'--args',
62-
`--user-data-dir=${userDataDir}`,
63-
'--hide-crash-restore-bubble',
64-
'--test-type',
65-
'--no-default-browser-check',
66-
'--no-first-run',
67-
'--disable-background-networking',
68-
'--disable-component-update',
69-
'--disable-search-engine-choice-screen',
70-
`--proxy-server=http://localhost:${appSettings.proxy.port}`,
71-
`--ignore-certificate-errors-spki-list=${certificateSPKI}`,
72-
disableChromeOptimizations,
73-
url?.trim() || 'about:blank',
74-
],
92+
args: args,
7593
onExit: sendBrowserClosedEvent,
7694
})
7795
}
96+
97+
function getChromiumPath(): Promise<string> {
98+
return new Promise((resolve, reject) => {
99+
exec('which chromium', (error, stdout, stderr) => {
100+
if (error) {
101+
log.error(error)
102+
return reject(error)
103+
}
104+
if (stderr) {
105+
log.error(stderr)
106+
return reject(stderr)
107+
}
108+
109+
return resolve(stdout.trim())
110+
})
111+
})
112+
}

src/logger.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from 'node:path'
44
import { FSWatcher, watch } from 'chokidar'
55
import { BrowserWindow } from 'electron'
66
import fs from 'fs/promises'
7+
import { getPlatform } from './utils/electron'
78

89
let watcher: FSWatcher
910

@@ -34,8 +35,9 @@ export function openLogFolder() {
3435
const logFile = log.transports.file.getFile().path
3536
const logPath = path.dirname(logFile)
3637

37-
// supports only Mac and Windows at this time
38-
const executable = process.platform === 'darwin' ? 'open' : 'explorer'
38+
const executable = ['mac', 'linux'].includes(getPlatform())
39+
? 'open'
40+
: 'explorer'
3941
spawn(executable, [logPath])
4042
}
4143

src/main.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import { DataFilePreview } from './types/testData'
6666
import { parseDataFile } from './utils/dataFile'
6767
import { createNewGeneratorFile } from './utils/generator'
6868
import { GeneratorFileDataSchema } from './schemas/generator'
69+
import { ChildProcessWithoutNullStreams } from 'child_process'
6970
import { COPYFILE_EXCL } from 'constants'
7071

7172
if (process.env.NODE_ENV !== 'development') {
@@ -102,7 +103,7 @@ let currentClientRoute = '/'
102103
let wasAppClosedByClient = false
103104
export let appSettings = defaultSettings
104105

105-
let currentBrowserProcess: Process | null
106+
let currentBrowserProcess: Process | ChildProcessWithoutNullStreams | null
106107
let currentk6Process: K6Process | null
107108
let watcher: FSWatcher
108109
let splashscreenWindow: BrowserWindow
@@ -318,8 +319,16 @@ ipcMain.handle('browser:start', async (event, url?: string) => {
318319

319320
ipcMain.on('browser:stop', async () => {
320321
console.info('browser:stop event received')
322+
321323
if (currentBrowserProcess) {
322-
await currentBrowserProcess.close()
324+
// macOS & windows
325+
if ('close' in currentBrowserProcess) {
326+
await currentBrowserProcess.close()
327+
// linux
328+
} else {
329+
currentBrowserProcess.kill()
330+
}
331+
323332
currentBrowserProcess = null
324333
}
325334
})
@@ -510,9 +519,9 @@ ipcMain.on('ui:toggle-theme', () => {
510519
nativeTheme.themeSource = nativeTheme.shouldUseDarkColors ? 'light' : 'dark'
511520
})
512521

513-
ipcMain.handle('ui:detect-browser', () => {
522+
ipcMain.handle('ui:detect-browser', async () => {
514523
try {
515-
const browserPath = getBrowserPath()
524+
const browserPath = await getBrowserPath()
516525
return browserPath !== ''
517526
} catch {
518527
log.error('Failed to find browser executable')

src/proxy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export const getCertificatesPath = () => {
125125
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
126126
return path.join(app.getAppPath(), 'resources', 'certificates')
127127
} else {
128-
return path.join(process.resourcesPath, 'certificates')
128+
return path.join(app.getPath('userData'), 'certificates')
129129
}
130130
}
131131

src/settings.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AppSettingsSchema } from './schemas/settings'
66
import { existsSync, readFileSync } from 'fs'
77
import { safeJsonParse } from './utils/json'
88
import log from 'electron-log/main'
9+
import { getPlatform } from './utils/electron'
910

1011
export const defaultSettings: AppSettings = {
1112
version: '3.0',
@@ -123,11 +124,16 @@ function isSettingsJsonObject() {
123124
}
124125

125126
export async function selectBrowserExecutable() {
126-
const extensions = process.platform === 'darwin' ? ['app'] : ['exe']
127+
const extensions = {
128+
mac: ['app'],
129+
win: ['exe'],
130+
linux: ['*'],
131+
}
132+
127133
return dialog.showOpenDialog({
128134
title: 'Select browser executable',
129135
properties: ['openFile'],
130-
filters: [{ name: 'Executables', extensions }],
136+
filters: [{ name: 'Executables', extensions: extensions[getPlatform()] }],
131137
})
132138
}
133139

0 commit comments

Comments
 (0)