Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download javy and function-runner directly #4190

Merged
merged 3 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 203 additions & 0 deletions packages/app/src/cli/services/function/binaries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import {javyBinary, functionRunnerBinary} from './binaries.js'
import {describe, expect, test} from 'vitest'

const javy = javyBinary()
const functionRunner = functionRunnerBinary()

describe('javy', () => {
test('properties are set correctly', () => {
expect(javy.name).toBe('javy')
expect(javy.version).match(/^v\d\.\d\.\d$/)
expect(javy.path).toMatch(/(\/|\\)javy$/)
})

describe('downloadUrl returns the correct URL', () => {
test('for Apple Silicon MacOS', () => {
// When
const url = javy.downloadUrl('darwin', 'arm64')

// Then
expect(url).toMatch(
/https:\/\/github.com\/bytecodealliance\/javy\/releases\/download\/v\d\.\d\.\d\/javy-arm-macos-v\d\.\d\.\d\.gz/,
)
})

test('for Intel MacOS', () => {
// When
const url = javy.downloadUrl('darwin', 'x64')

// Then
expect(url).toMatch(
/https:\/\/github.com\/bytecodealliance\/javy\/releases\/download\/v\d\.\d\.\d\/javy-x86_64-macos-v\d\.\d\.\d\.gz/,
)
})

test('for Arm Linux', () => {
// When
const url = javy.downloadUrl('linux', 'arm64')

// Then
expect(url).toMatch(
/https:\/\/github.com\/bytecodealliance\/javy\/releases\/download\/v\d\.\d\.\d\/javy-arm-linux-v\d\.\d\.\d\.gz/,
)
})

test('for x86 Linux', () => {
// When
const url = javy.downloadUrl('linux', 'x64')

// Then
expect(url).toMatch(
/https:\/\/github.com\/bytecodealliance\/javy\/releases\/download\/v\d\.\d\.\d\/javy-x86_64-linux-v\d\.\d\.\d\.gz/,
)
})

test('for a 32-bit installation of NodeJS on Windows', () => {
// When
const url = javy.downloadUrl('win32', 'ia32')

// Uses the 64-bit version since we assume the operating system actually uses 64-bits and its just a 32-bit
// installation of NodeJS.
// Then
expect(url).toMatch(
/https:\/\/github.com\/bytecodealliance\/javy\/releases\/download\/v\d\.\d\.\d\/javy-x86_64-windows-v\d\.\d\.\d\.gz/,
)
})

test('for a 64-bit installation of NodeJS on Windows', () => {
// When
const url = javy.downloadUrl('win32', 'x64')

// Then
expect(url).toMatch(
/https:\/\/github.com\/bytecodealliance\/javy\/releases\/download\/v\d\.\d\.\d\/javy-x86_64-windows-v\d\.\d\.\d\.gz/,
)
})

test('aix (or any other unsupported platform) throws an error', () => {
// When
const platform = 'aix'

// Then
expect(() => javy.downloadUrl(platform, 'x64')).toThrowError('Unsupported platform aix')
})

test('ppc architecture (or any other unsupported architecture) throws an error', () => {
// When
const arch = 'ppc'

// Then
expect(() => javy.downloadUrl('darwin', arch)).toThrowError('Unsupported architecture ppc')
})

test('Unsupported combination throws an error', () => {
// When
const arch = 'arm'
const platform = 'win32'

// Then
expect(() => javy.downloadUrl(platform, arch)).toThrowError(
'Unsupported platform/architecture combination win32/arm',
)
})
})
})

describe('functionRunner', () => {
test('properties are set correctly', () => {
expect(functionRunner.name).toBe('function-runner')
expect(functionRunner.version).match(/^v\d\.\d\.\d$/)
expect(functionRunner.path).toMatch(/(\/|\\)function-runner$/)
})

describe('downloadUrl returns the correct URL', () => {
test('for Apple Silicon MacOS', () => {
// When
const url = functionRunner.downloadUrl('darwin', 'arm64')

// Then
expect(url).toMatch(
/https:\/\/github.com\/Shopify\/function-runner\/releases\/download\/v\d\.\d\.\d\/function-runner-arm-macos-v\d\.\d\.\d\.gz/,
)
})

test('for Intel MacOS', () => {
// When
const url = functionRunner.downloadUrl('darwin', 'x64')

// Then
expect(url).toMatch(
/https:\/\/github.com\/Shopify\/function-runner\/releases\/download\/v\d\.\d\.\d\/function-runner-x86_64-macos-v\d\.\d\.\d\.gz/,
)
})

test('for Arm Linux', () => {
// When
const url = functionRunner.downloadUrl('linux', 'arm64')

// Then
expect(url).toMatch(
/https:\/\/github.com\/Shopify\/function-runner\/releases\/download\/v\d\.\d\.\d\/function-runner-arm-linux-v\d\.\d\.\d\.gz/,
)
})

test('for x86 Linux', () => {
// When
const url = functionRunner.downloadUrl('linux', 'x64')

// Then
expect(url).toMatch(
/https:\/\/github.com\/Shopify\/function-runner\/releases\/download\/v\d\.\d\.\d\/function-runner-x86_64-linux-v\d\.\d\.\d\.gz/,
)
})

test('for a 32-bit installation of NodeJS on Windows', () => {
// When
const url = functionRunner.downloadUrl('win32', 'ia32')

// Uses the 64-bit version since we assume the operating system actually uses 64-bits and its just a 32-bit
// installation of NodeJS.
// Then
expect(url).toMatch(
/https:\/\/github.com\/Shopify\/function-runner\/releases\/download\/v\d\.\d\.\d\/function-runner-x86_64-windows-v\d\.\d\.\d\.gz/,
)
})

test('for a 64-bit installation of NodeJS on Windows', () => {
// When
const url = functionRunner.downloadUrl('win32', 'x64')

// Then
expect(url).toMatch(
/https:\/\/github.com\/Shopify\/function-runner\/releases\/download\/v\d\.\d\.\d\/function-runner-x86_64-windows-v\d\.\d\.\d\.gz/,
)
})

test('aix (or any other unsupported platform) throws an error', () => {
// When
const platform = 'aix'

// Then
expect(() => functionRunner.downloadUrl(platform, 'x64')).toThrowError('Unsupported platform aix')
})

test('ppc architecture (or any other unsupported architecture) throws an error', () => {
// When
const arch = 'ppc'

// Then
expect(() => functionRunner.downloadUrl('darwin', arch)).toThrowError('Unsupported architecture ppc')
})

test('Unsupported combination throws an error', () => {
// When
const arch = 'arm'
const platform = 'win32'

// Then
expect(() => functionRunner.downloadUrl(platform, arch)).toThrowError(
'Unsupported platform/architecture combination win32/arm',
)
})
})
})
115 changes: 115 additions & 0 deletions packages/app/src/cli/services/function/binaries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {joinPath, dirname} from '@shopify/cli-kit/node/path'
import {chmod, createFileWriteStream, fileExists, mkdir} from '@shopify/cli-kit/node/fs'
import {outputDebug} from '@shopify/cli-kit/node/output'
import {PipelineSource} from 'stream'
import stream from 'node:stream/promises'
import fs from 'node:fs'
import * as gzip from 'node:zlib'
import {fileURLToPath} from 'node:url'

const JAVY_VERSION = 'v3.0.1'
const FUNCTION_RUNNER_VERSION = 'v5.1.3'

// The logic for determining the download URL and what to do with the response stream is _coincidentally_ the same for
// Javy and function-runner for now. Those methods may not continue to have the same logic in the future. If they
// diverge, make `Binary` an abstract class and create subclasses to handle the different logic polymorphically.
class DownloadableBinary {
readonly name: string
readonly version: string
readonly path: string
private readonly gitHubRepo: string

constructor(name: string, version: string, gitHubRepo: string) {
this.name = name
this.version = version
this.path = joinPath(dirname(fileURLToPath(import.meta.url)), '..', 'bin', `${this.name}`)
this.gitHubRepo = gitHubRepo
}

downloadUrl(processPlatform: string, processArch: string) {
let platform
let arch
switch (processPlatform.toLowerCase()) {
case 'darwin':
platform = 'macos'
break
case 'linux':
platform = 'linux'
break
case 'win32':
platform = 'windows'
break
default:
throw Error(`Unsupported platform ${processPlatform}`)
}
switch (processArch.toLowerCase()) {
case 'arm':
case 'arm64':
arch = 'arm'
break
// A 32 bit arch likely needs that someone has 32bit Node installed on a
// 64 bit system, and wasmtime doesn't support 32bit anyway.
case 'ia32':
case 'x64':
arch = 'x86_64'
break
default:
throw Error(`Unsupported architecture ${processArch}`)
}

const archPlatform = `${arch}-${platform}`
// These are currently the same between both binaries _coincidentally_.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this coincidence something that might change when we migrate to a newer version?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that would be the only time it could possibly change. I don't foresee it changing the near future.

Copy link
Contributor

@DuncanUszkay1 DuncanUszkay1 Jul 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it- sounds like it's worth adding a step in the version upgrade process doc to check on this detail

const supportedTargets = ['arm-linux', 'arm-macos', 'x86_64-macos', 'x86_64-windows', 'x86_64-linux']
if (!supportedTargets.includes(archPlatform)) {
throw Error(`Unsupported platform/architecture combination ${processPlatform}/${processArch}`)
}

return `https://github.com/${this.gitHubRepo}/releases/download/${this.version}/${this.name}-${archPlatform}-${this.version}.gz`
}

async processResponse(responseStream: PipelineSource<unknown>, outputStream: fs.WriteStream): Promise<void> {
const gunzip = gzip.createGunzip()
await stream.pipeline(responseStream, gunzip, outputStream)
}
}

let _javy: DownloadableBinary
let _functionRunner: DownloadableBinary

export function javyBinary() {
if (!_javy) {
_javy = new DownloadableBinary('javy', JAVY_VERSION, 'bytecodealliance/javy')
}
return _javy
}

export function functionRunnerBinary() {
if (!_functionRunner) {
_functionRunner = new DownloadableBinary('function-runner', FUNCTION_RUNNER_VERSION, 'Shopify/function-runner')
}
return _functionRunner
}

export async function installBinary(bin: DownloadableBinary) {
const isInstalled = await fileExists(bin.path)
if (isInstalled) {
return
}

const url = bin.downloadUrl(process.platform, process.arch)
outputDebug(`Downloading ${bin.name} ${bin.version} from ${url} to ${bin.path}`)
await mkdir(dirname(bin.path))
const resp = await fetch(url)
if (resp.status !== 200) {
throw new Error(`Downloading ${bin.name} failed with status code of ${resp.status}`)
}

const responseStream = resp.body
if (responseStream === null) {
throw new Error(`Downloading ${bin.name} failed with empty response body`)
}

const outputStream = createFileWriteStream(bin.path)
await bin.processResponse(responseStream, outputStream)
await chmod(bin.path, 0o775)
}
Loading