Skip to content

Commit

Permalink
fix: prisma --version (#13737) (#14053)
Browse files Browse the repository at this point in the history
* chore: try 1

* chore(internals): rename getVersion to getEngineVersion

* chore: try 2

* chore: try 3

* chore: try 4

* eureka

* Update packages/internals/src/engine-commands/getEnginesMetaInfo.ts

* ci: fix tests

* chore: renamed E_CANNOT_RESOLVE_VERSION

* cli: removed duplicate entry from version command

* cli: updated version snapshot

* chore: updated version snapshot to consider binary
  • Loading branch information
jkomyno committed Jul 4, 2022
1 parent 131d75a commit 60b7b77
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 54 deletions.
77 changes: 36 additions & 41 deletions packages/cli/src/Version.ts
Expand Up @@ -4,32 +4,23 @@ import type { Command } from '@prisma/internals'
import {
arg,
BinaryType,
engineEnvVarMap,
format,
formatTable,
getConfig,
getEngineVersion,
getEnginesMetaInfo,
getSchema,
getSchemaPath,
HelpError,
isError,
loadEnvFile,
resolveBinary,
} from '@prisma/internals'
import chalk from 'chalk'
import fs from 'fs'
import path from 'path'
import { match, P } from 'ts-pattern'

import { getInstalledPrismaClientVersion } from './utils/getClientVersion'

const packageJson = require('../package.json') // eslint-disable-line @typescript-eslint/no-var-requires

interface BinaryInfo {
path: string
version: string
fromEnvVar?: string
}

/**
* $ prisma version
*/
Expand Down Expand Up @@ -74,29 +65,51 @@ export class Version implements Command {

const platform = await getPlatform()
const cliQueryEngineBinaryType = getCliQueryEngineBinaryType()
const introspectionEngine = await this.resolveEngine(BinaryType.introspectionEngine)
const migrationEngine = await this.resolveEngine(BinaryType.migrationEngine)
// TODO This conditional does not really belong here, CLI should be able to tell you which engine it is _actually_ using
const queryEngine = await this.resolveEngine(cliQueryEngineBinaryType)
const fmtBinary = await this.resolveEngine(BinaryType.prismaFmt)

const [enginesMetaInfo, enginesMetaInfoErrors] = await getEnginesMetaInfo()

const enginesRows = enginesMetaInfo.map((engineMetaInfo) => {
return match(engineMetaInfo)
.with({ 'query-engine': P.select() }, (currEngineInfo) => {
return [
`Query Engine${cliQueryEngineBinaryType === BinaryType.libqueryEngine ? ' (Node-API)' : ' (Binary)'}`,
currEngineInfo,
]
})
.with({ 'migration-engine': P.select() }, (currEngineInfo) => {
return ['Migration Engine', currEngineInfo]
})
.with({ 'introspection-engine': P.select() }, (currEngineInfo) => {
return ['Introspection Engine', currEngineInfo]
})
.with({ 'format-binary': P.select() }, (currEngineInfo) => {
return ['Format Binary', currEngineInfo]
})
.exhaustive()
})

const prismaClientVersion = await getInstalledPrismaClientVersion()

const rows = [
[packageJson.name, packageJson.version],
['@prisma/client', prismaClientVersion ?? 'Not found'],
['Current platform', platform],
[
`Query Engine${cliQueryEngineBinaryType === BinaryType.libqueryEngine ? ' (Node-API)' : ' (Binary)'}`,
this.printBinaryInfo(queryEngine),
],
['Migration Engine', this.printBinaryInfo(migrationEngine)],
['Introspection Engine', this.printBinaryInfo(introspectionEngine)],
['Format Binary', this.printBinaryInfo(fmtBinary)],

...enginesRows,

['Default Engines Hash', enginesVersion],
['Studio', packageJson.devDependencies['@prisma/studio-server']],
]

/**
* If reading Rust engines metainfo (like their git hash) failed, display the errors to stderr,
* and let Node.js exit naturally, but with error code 1.
*/
if (enginesMetaInfoErrors.length > 0) {
process.exitCode = 1
enginesMetaInfoErrors.forEach((e) => console.error(e))
}

const schemaPath = await getSchemaPath()
const featureFlags = await this.getFeatureFlags(schemaPath)

Expand Down Expand Up @@ -127,24 +140,6 @@ export class Version implements Command {
return []
}

private printBinaryInfo({ path: absolutePath, version, fromEnvVar }: BinaryInfo): string {
const resolved = fromEnvVar ? `, resolved by ${fromEnvVar}` : ''
return `${version} (at ${path.relative(process.cwd(), absolutePath)}${resolved})`
}

private async resolveEngine(binaryName: BinaryType): Promise<BinaryInfo> {
const envVar = engineEnvVarMap[binaryName]
const pathFromEnv = process.env[envVar]
if (pathFromEnv && fs.existsSync(pathFromEnv)) {
const version = await getEngineVersion(pathFromEnv, binaryName)
return { version, path: pathFromEnv, fromEnvVar: envVar }
}

const binaryPath = await resolveBinary(binaryName)
const version = await getEngineVersion(binaryPath, binaryName)
return { path: binaryPath, version }
}

public help(error?: string): string | HelpError {
if (error) {
return new HelpError(`\n${chalk.bold.red(`!`)} ${error}\n${Version.help}`)
Expand Down
Expand Up @@ -12,18 +12,6 @@ Default Engines Hash : ENGINE_VERSION
Studio : STUDIO_VERSION
`;

exports[`version basic version 1`] = `
prisma : 0.0.0
@prisma/client : Not found
Current platform : TEST_PLATFORM
Query Engine (Binary) : query-engine ENGINE_VERSION (at sanitized_path/query-engine-TEST_PLATFORM)
Migration Engine : migration-engine-cli ENGINE_VERSION (at sanitized_path/migration-engine-TEST_PLATFORM)
Introspection Engine : introspection-core ENGINE_VERSION (at sanitized_path/introspection-engine-TEST_PLATFORM)
Format Binary : prisma-fmt ENGINE_VERSION (at sanitized_path/prisma-fmt-TEST_PLATFORM)
Default Engines Hash : ENGINE_VERSION
Studio : STUDIO_VERSION
`;

exports[`version version with custom binaries (Node-API) 1`] = `
prisma : 0.0.0
@prisma/client : Not found
Expand All @@ -36,6 +24,18 @@ Default Engines Hash : ENGINE_VERSION
Studio : STUDIO_VERSION
`;

exports[`version basic version 1`] = `
prisma : 0.0.0
@prisma/client : Not found
Current platform : TEST_PLATFORM
Query Engine (Binary) : query-engine ENGINE_VERSION (at sanitized_path/query-engine-TEST_PLATFORM)
Migration Engine : migration-engine-cli ENGINE_VERSION (at sanitized_path/migration-engine-TEST_PLATFORM)
Introspection Engine : introspection-core ENGINE_VERSION (at sanitized_path/introspection-engine-TEST_PLATFORM)
Format Binary : prisma-fmt ENGINE_VERSION (at sanitized_path/prisma-fmt-TEST_PLATFORM)
Default Engines Hash : ENGINE_VERSION
Studio : STUDIO_VERSION
`;

exports[`version version with custom binaries 1`] = `
prisma : 0.0.0
@prisma/client : Not found
Expand Down
8 changes: 8 additions & 0 deletions packages/internals/src/engine-commands/getEngineVersion.ts
Expand Up @@ -3,6 +3,7 @@ import { getCliQueryEngineBinaryType } from '@prisma/engines'
import { BinaryType } from '@prisma/fetch-engine'
import { isNodeAPISupported } from '@prisma/get-platform'
import execa from 'execa'
import * as TE from 'fp-ts/TaskEither'

import { resolveBinary } from '../resolveBinary'
import { load } from '../utils/load'
Expand All @@ -27,3 +28,10 @@ export async function getEngineVersion(enginePath?: string, binaryName?: BinaryT
return result.stdout
}
}

export function safeGetEngineVersion(enginePath?: string, binaryName?: BinaryType): TE.TaskEither<Error, string> {
return TE.tryCatch(
() => getEngineVersion(enginePath, binaryName),
(error) => error as Error,
)
}
178 changes: 178 additions & 0 deletions packages/internals/src/engine-commands/getEnginesMetaInfo.ts
@@ -0,0 +1,178 @@
import { getCliQueryEngineBinaryType } from '@prisma/engines'
import { BinaryType } from '@prisma/fetch-engine'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/lib/function'
import * as O from 'fp-ts/Option'
import * as TE from 'fp-ts/TaskEither'
import fs from 'fs'
import path from 'path'
import { match, P } from 'ts-pattern'

import { engineEnvVarMap, safeResolveBinary } from '../resolveBinary'
import { safeGetEngineVersion } from './getEngineVersion'

/**
* Both an engine binary and a library might be resolved from and environment variable indicating a path.
* We first try to retrieve the engines' path from an env var; if it fails, we fall back to `safeResolveBinary`.
* If both fail, we return an error.
* Even if we resolve a path, retrieving the version might fail.
*/

export type EngineInfo = {
fromEnvVar: O.Option<string>
path: E.Either<Error, string>
version: E.Either<Error, string>
}

export type BinaryMatrix<T> = {
'query-engine': T
'migration-engine': T
'introspection-engine': T
'format-binary': T
}

export type BinaryInfoMatrix = BinaryMatrix<EngineInfo>

export async function getEnginesMetaInfo() {
const cliQueryEngineBinaryType = getCliQueryEngineBinaryType()

const engines = [
{
name: 'query-engine' as const,
type: cliQueryEngineBinaryType,
},
{
name: 'migration-engine' as const,
type: BinaryType.migrationEngine,
},
{
name: 'introspection-engine' as const,
type: BinaryType.introspectionEngine,
},
{
name: 'format-binary' as const,
type: BinaryType.prismaFmt,
},
] as const

/**
* Resolve `resolveEngine` promises (that can never fail) and forward a reference to
* the engine name in the promise result.
*/
const enginePromises = engines.map(({ name, type }) => {
return resolveEngine(type).then((result) => [name, result])
})
const engineMatrix: BinaryInfoMatrix = await Promise.all(enginePromises).then(Object.fromEntries)

const engineDataAcc = engines.map(({ name }) => {
const [engineInfo, errors] = getEnginesInfo(engineMatrix[name])
return [{ [name]: engineInfo } as { [name in keyof BinaryInfoMatrix]: string }, errors] as const
})

// map each engine to its version
const engineMetaInfo: {
'query-engine': string
'migration-engine': string
'introspection-engine': string
'format-binary': string
}[] = engineDataAcc.map((arr) => arr[0])

// keep track of any error that has occurred, if any
const enginesMetaInfoErrors: Error[] = engineDataAcc.flatMap((arr) => arr[1])

return [engineMetaInfo, enginesMetaInfoErrors] as const
}

export function getEnginesInfo(enginesInfo: EngineInfo): readonly [string, Error[]] {
// if the engine is not found, or the version cannot be retrieved, keep track of the resulting errors.
const errors = [] as Error[]

// compute message displayed when an engine is resolved via env vars
const resolved = match(enginesInfo)
.with({ fromEnvVar: P.when(O.isSome) }, (_engineInfo) => {
return `, resolved by ${_engineInfo.fromEnvVar.value}`
})
.otherwise(() => '')

// compute absolute path of an engine, returning an error message and populating
// `errors` if it fails
const absolutePath = match(enginesInfo)
.with({ path: P.when(E.isRight) }, (_engineInfo) => {
return _engineInfo.path.right
})
.with({ path: P.when(E.isLeft) }, (_engineInfo) => {
// the binary/library can't be found
errors.push(_engineInfo.path.left)
return 'E_CANNOT_RESOLVE_PATH' as const
})
.exhaustive()

// compute version (git hash) of an engine, returning an error message and populating
// `errors` if it fails
const version = match(enginesInfo)
.with({ version: P.when(E.isRight) }, (_engineInfo) => {
return _engineInfo.version.right
})
.with({ version: P.when(E.isLeft) }, (_engineInfo) => {
// extracting the version failed
errors.push(_engineInfo.version.left)
return 'E_CANNOT_RESOLVE_VERSION' as const
})
.exhaustive()

const versionMessage = `${version} (at ${path.relative(process.cwd(), absolutePath)}${resolved})`
return [versionMessage, errors] as const
}

/**
* An engine path read from the environment is valid only if it exists on disk.
* @param pathFromEnv engine path read from process.env
*/
function isPathFromEnvValid(pathFromEnv: string | undefined): pathFromEnv is string {
return !!pathFromEnv && fs.existsSync(pathFromEnv)
}

export async function resolveEngine(binaryName: BinaryType): Promise<EngineInfo> {
const envVar = engineEnvVarMap[binaryName]
const pathFromEnv = process.env[envVar]

/**
* Read the binary path, preferably from the environment, or resolving the canonical path
* from the given `binaryName`.
*/

const pathFromEnvOption: O.Option<string> = O.fromPredicate(isPathFromEnvValid)(pathFromEnv)

const fromEnvVarOption: O.Option<string> = pipe(
pathFromEnvOption,
O.map(() => envVar),
)

const enginePathEither: E.Either<Error, string> = await pipe(
pathFromEnvOption,
O.fold(
() => safeResolveBinary(binaryName),
(_pathFromEnv) => TE.right(_pathFromEnv),
),
)()

/**
* Read the version from the engine, but only if the enginePath is valid.
*/

const versionEither: E.Either<Error, string> = await pipe(
enginePathEither,
TE.fromEither,
TE.chain((enginePath) => {
return safeGetEngineVersion(enginePath, binaryName)
}),
)()

const engineInfo: EngineInfo = {
path: enginePathEither,
version: versionEither,
fromEnvVar: fromEnvVarOption,
}

return engineInfo
}
1 change: 1 addition & 0 deletions packages/internals/src/engine-commands/index.ts
Expand Up @@ -3,4 +3,5 @@ export type { ConfigMetaFormat } from './getConfig'
export { getConfig } from './getConfig'
export type { GetDMMFOptions } from './getDmmf'
export { getDMMF } from './getDmmf'
export { getEnginesMetaInfo } from './getEnginesMetaInfo'
export { getEngineVersion } from './getEngineVersion'
9 changes: 8 additions & 1 deletion packages/internals/src/resolveBinary.ts
Expand Up @@ -3,6 +3,7 @@ import { plusX } from '@prisma/engine-core'
import { getEnginesPath } from '@prisma/engines'
import { BinaryType } from '@prisma/fetch-engine'
import { getNodeAPIName, getPlatform } from '@prisma/get-platform'
import * as TE from 'fp-ts/TaskEither'
import fs from 'fs'
import makeDir from 'make-dir'
import path from 'path'
Expand All @@ -11,7 +12,6 @@ import { promisify } from 'util'

const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile)
const debug = Debug('prisma:resolveBinary')

async function getBinaryName(name: BinaryType): Promise<string> {
const platform = await getPlatform()
Expand Down Expand Up @@ -81,6 +81,13 @@ export async function resolveBinary(name: BinaryType, proposedPath?: string): Pr
)
}

export function safeResolveBinary(name: BinaryType, proposedPath?: string): TE.TaskEither<Error, string> {
return TE.tryCatch(
() => resolveBinary(name, proposedPath),
(error) => error as Error,
)
}

export async function maybeCopyToTmp(file: string): Promise<string> {
const dir = eval('__dirname')

Expand Down

0 comments on commit 60b7b77

Please sign in to comment.