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

feat: adds checkDevEngines #116

Merged
merged 22 commits into from
Sep 11, 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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,10 @@ Check if a package's `os`, `cpu` and `libc` match the running system.
`environment` overrides the execution environment which comes from `process.platform` `process.arch` and current `libc` environment by default. `environment.os` `environment.cpu` and `environment.libc` are available.

Error code: 'EBADPLATFORM'


### .checkDevEngines(wanted, current, opts)

Check if a package's `devEngines` property matches the current system environment.

Returns an array of `Error` objects, some of which may be warnings, this can be checked with `.isError` and `.isWarn`. Errors correspond to an error for a given "engine" failure, reasons for each engine "dependency" failure can be found within `.errors`.
60 changes: 60 additions & 0 deletions lib/current-env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const process = require('node:process')
const nodeOs = require('node:os')

function isMusl (file) {
return file.includes('libc.musl-') || file.includes('ld-musl-')
}

function os () {
return process.platform
}

function cpu () {
return process.arch
}

function libc (osName) {
// this is to make it faster on non linux machines
if (osName !== 'linux') {
return undefined
}
let family
const report = process.report.getReport()
wraithgar marked this conversation as resolved.
Show resolved Hide resolved
if (report.header?.glibcVersionRuntime) {
family = 'glibc'
} else if (Array.isArray(report.sharedObjects) && report.sharedObjects.some(isMusl)) {
family = 'musl'
}
return family
}

function devEngines (env = {}) {
const osName = env.os || os()
return {
cpu: {
name: env.cpu || cpu(),
},
libc: {
name: env.libc || libc(osName),
},
os: {
name: osName,
version: env.osVersion || nodeOs.release(),
},
packageManager: {
name: 'npm',
version: env.npmVersion,
},
runtime: {
name: 'node',
version: env.nodeVersion || process.version,
},
}
}

module.exports = {
cpu,
libc,
os,
devEngines,
}
145 changes: 145 additions & 0 deletions lib/dev-engines.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
const satisfies = require('semver/functions/satisfies')
const validRange = require('semver/ranges/valid')

const recognizedOnFail = [
'ignore',
'warn',
'error',
'download',
]

const recognizedProperties = [
'name',
'version',
'onFail',
]

const recognizedEngines = [
'packageManager',
'runtime',
'cpu',
'libc',
'os',
]

/** checks a devEngine dependency */
function checkDependency (wanted, current, opts) {
const { engine } = opts

if ((typeof wanted !== 'object' || wanted === null) || Array.isArray(wanted)) {
throw new Error(`Invalid non-object value for "${engine}"`)
}

const properties = Object.keys(wanted)

for (const prop of properties) {
if (!recognizedProperties.includes(prop)) {
throw new Error(`Invalid property "${prop}" for "${engine}"`)
}
}

if (!properties.includes('name')) {
throw new Error(`Missing "name" property for "${engine}"`)
}

if (typeof wanted.name !== 'string') {
throw new Error(`Invalid non-string value for "name" within "${engine}"`)
}

if (typeof current.name !== 'string' || current.name === '') {
throw new Error(`Unable to determine "name" for "${engine}"`)
}

if (properties.includes('onFail')) {
if (typeof wanted.onFail !== 'string') {
throw new Error(`Invalid non-string value for "onFail" within "${engine}"`)
}
if (!recognizedOnFail.includes(wanted.onFail)) {
throw new Error(`Invalid onFail value "${wanted.onFail}" for "${engine}"`)
}
}

if (wanted.name !== current.name) {
return new Error(
`Invalid name "${wanted.name}" does not match "${current.name}" for "${engine}"`
)
}

if (properties.includes('version')) {
if (typeof wanted.version !== 'string') {
throw new Error(`Invalid non-string value for "version" within "${engine}"`)
}
if (typeof current.version !== 'string' || current.version === '') {
throw new Error(`Unable to determine "version" for "${engine}" "${wanted.name}"`)
}
if (validRange(wanted.version)) {
if (!satisfies(current.version, wanted.version, opts.semver)) {
return new Error(
// eslint-disable-next-line max-len
`Invalid semver version "${wanted.version}" does not match "${current.version}" for "${engine}"`
)
}
} else if (wanted.version !== current.version) {
return new Error(
`Invalid version "${wanted.version}" does not match "${current.version}" for "${engine}"`
)
}
}
}

/** checks devEngines package property and returns array of warnings / errors */
function checkDevEngines (wanted, current = {}, opts = {}) {
if ((typeof wanted !== 'object' || wanted === null) || Array.isArray(wanted)) {
throw new Error(`Invalid non-object value for devEngines`)
}

const errors = []

for (const engine of Object.keys(wanted)) {
if (!recognizedEngines.includes(engine)) {
throw new Error(`Invalid property "${engine}"`)
}
const dependencyAsAuthored = wanted[engine]
const dependencies = [dependencyAsAuthored].flat()
const currentEngine = current[engine] || {}

// this accounts for empty array eg { runtime: [] } and ignores it
if (dependencies.length === 0) {
continue
}

const depErrors = []
for (const dep of dependencies) {
const result = checkDependency(dep, currentEngine, { ...opts, engine })
if (result) {
depErrors.push(result)
}
}

const invalid = depErrors.length === dependencies.length

if (invalid) {
const lastDependency = dependencies[dependencies.length - 1]
let onFail = lastDependency.onFail || 'error'
if (onFail === 'download') {
onFail = 'error'
}

const err = Object.assign(new Error(`Invalid engine "${engine}"`), {
errors: depErrors,
engine,
isWarn: onFail === 'warn',
isError: onFail === 'error',
current: currentEngine,
required: dependencyAsAuthored,
})

errors.push(err)
}
}
return errors
}

module.exports = {
checkDevEngines,
}
41 changes: 15 additions & 26 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const semver = require('semver')
const currentEnv = require('./current-env')
const { checkDevEngines } = require('./dev-engines')

const checkEngine = (target, npmVer, nodeVer, force = false) => {
const nodev = force ? null : nodeVer
Expand All @@ -20,44 +22,29 @@ const checkEngine = (target, npmVer, nodeVer, force = false) => {
}
}

const isMusl = (file) => file.includes('libc.musl-') || file.includes('ld-musl-')

const checkPlatform = (target, force = false, environment = {}) => {
if (force) {
return
}

const platform = environment.os || process.platform
const arch = environment.cpu || process.arch
const osOk = target.os ? checkList(platform, target.os) : true
const cpuOk = target.cpu ? checkList(arch, target.cpu) : true
const os = environment.os || currentEnv.os()
const cpu = environment.cpu || currentEnv.cpu()
const libc = environment.libc || currentEnv.libc(os)

let libcOk = true
let libcFamily = null
if (target.libc) {
// libc checks only work in linux, any value is a failure if we aren't
if (environment.libc) {
libcOk = checkList(environment.libc, target.libc)
} else if (platform !== 'linux') {
libcOk = false
} else {
const report = process.report.getReport()
if (report.header?.glibcVersionRuntime) {
libcFamily = 'glibc'
} else if (Array.isArray(report.sharedObjects) && report.sharedObjects.some(isMusl)) {
libcFamily = 'musl'
}
libcOk = libcFamily ? checkList(libcFamily, target.libc) : false
}
const osOk = target.os ? checkList(os, target.os) : true
const cpuOk = target.cpu ? checkList(cpu, target.cpu) : true
let libcOk = target.libc ? checkList(libc, target.libc) : true
if (target.libc && !libc) {
libcOk = false
}

if (!osOk || !cpuOk || !libcOk) {
throw Object.assign(new Error('Unsupported platform'), {
pkgid: target._id,
current: {
os: platform,
cpu: arch,
libc: libcFamily,
os,
cpu,
libc,
},
required: {
os: target.os,
Expand Down Expand Up @@ -98,4 +85,6 @@ const checkList = (value, list) => {
module.exports = {
checkEngine,
checkPlatform,
checkDevEngines,
currentEnv,
}
Loading
Loading