Skip to content

Commit

Permalink
feat: adds checkDevEngines (#116)
Browse files Browse the repository at this point in the history
  • Loading branch information
reggi authored Sep 11, 2024
1 parent 3e0e1b6 commit ebf9b9f
Show file tree
Hide file tree
Showing 5 changed files with 646 additions and 26 deletions.
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()
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

0 comments on commit ebf9b9f

Please sign in to comment.