UNPKG

@cyclonedx/cdxgen

Version:

Creates CycloneDX Software Bill of Materials (SBOM) from source or container image

306 lines (296 loc) 9.32 kB
import { readdirSync, readFileSync } from "node:fs"; import { basename, dirname, join, sep } from "node:path"; import { thoughtLog } from "./logger.js"; import { DEBUG_MODE, PYTHON_CMD, safeExistsSync } from "./utils.js"; /** * Universal virtual environment metadata detector * @param {Object} env - Environment variables (defaults to process.env) * @param {string} [explicitPath] - Optional explicit venv path to inspect * @returns {Object} Structured environment metadata */ export function getVenvMetadata(env = process.env, explicitPath = null) { const result = { type: "system", // 'uv' | 'venv' | 'conda' | 'miniconda' | 'pyenv' | 'poetry' | 'pipenv' | 'virtualenv' | 'pixi' | 'bazel' | 'rye' | 'hatch' | 'pdm' | 'system' | 'unknown' path: null, isActive: false, pythonExecutable: null, pythonVersion: "unknown", pythonImplementation: null, toolVersion: null, uv: null, conda: null, pyenv: null, poetry: null, pipenv: null, pixi: null, }; let venvPath = explicitPath; if (!venvPath) { if (env.VIRTUAL_ENV) { venvPath = env.VIRTUAL_ENV; result.isActive = true; } else if (env.CONDA_PREFIX) { venvPath = env.CONDA_PREFIX; result.isActive = true; } else if (env.PIXI_PROJECT_ROOT && env.PIXI_ENVIRONMENT_NAME) { venvPath = join( env.PIXI_PROJECT_ROOT, ".pixi", "envs", env.PIXI_ENVIRONMENT_NAME, ); result.isActive = true; } else if (env.CONDA_PYTHON_EXE && safeExistsSync(env.CONDA_PYTHON_EXE)) { result.pythonExecutable = env.CONDA_PYTHON_EXE; result.type = "conda"; } } if (!venvPath) { return result; } result.path = venvPath; const isWin = process.platform === "win32"; const binDir = isWin ? "Scripts" : "bin"; const exeNames = isWin ? ["python.exe", "python3.exe"] : ["python", "python3"]; if (!isWin) { for (let minor = 16; minor >= 6; minor--) { exeNames.push(`python3.${minor}`); } } for (const exe of exeNames) { const candidate = join(venvPath, binDir, exe); if (safeExistsSync(candidate)) { result.pythonExecutable = candidate; break; } } if (!result.pythonExecutable && isWin) { const rootExe = join(venvPath, "python.exe"); if (safeExistsSync(rootExe)) { result.pythonExecutable = rootExe; } } if ( env.BUILD_WORKSPACE_DIRECTORY || venvPath.includes("bazel-out") || venvPath.includes(".runfiles") ) { result.type = "bazel"; return result; } const isLocalVenv = basename(venvPath) === ".venv"; const projectRoot = isLocalVenv ? dirname(venvPath) : null; const pyvenvCfgPath = join(venvPath, "pyvenv.cfg"); if (safeExistsSync(pyvenvCfgPath)) { const cfg = _parsePyvenvCfg(pyvenvCfgPath); result.pythonVersion = cfg.version_info || "unknown"; result.pythonImplementation = cfg.implementation || null; if (cfg.uv) { result.type = "uv"; result.toolVersion = cfg.uv; result.uv = { version: cfg.uv, home: cfg.home }; return result; } if ( env.POETRY_ACTIVE === "1" || venvPath.includes(`pypoetry${sep}virtualenvs`) || (projectRoot && safeExistsSync(join(projectRoot, "poetry.lock"))) ) { result.type = "poetry"; if (projectRoot) result.poetry = { projectRoot }; const lockFile = projectRoot ? join(projectRoot, "poetry.lock") : null; if (lockFile && safeExistsSync(lockFile)) { const poetryVersion = _extractPoetryVersion(lockFile); if (poetryVersion) result.toolVersion = poetryVersion; } return result; } if ( env.PIPENV_ACTIVE === "1" || venvPath.includes(`.virtualenvs${sep}`) || (projectRoot && safeExistsSync(join(projectRoot, "Pipfile"))) ) { result.type = "pipenv"; if (projectRoot) result.pipenv = { projectRoot }; return result; } if ( env.RYE_ACTIVE === "1" || (projectRoot && safeExistsSync(join(projectRoot, "requirements.lock")) && safeExistsSync(join(projectRoot, ".rye"))) ) { result.type = "rye"; return result; } if (env.HATCH_ENV_ACTIVE || venvPath.includes(`hatch${sep}env`)) { result.type = "hatch"; return result; } if ( env.PDM_ACTIVE === "1" || (projectRoot && safeExistsSync(join(projectRoot, "pdm.lock"))) ) { result.type = "pdm"; return result; } if (cfg.virtualenv) { result.type = "virtualenv"; result.toolVersion = cfg.virtualenv; } else { result.type = "venv"; } return result; } const condaMetaDir = join(venvPath, "conda-meta"); if (safeExistsSync(condaMetaDir)) { if (env.PIXI_PROJECT_ROOT || venvPath.includes(`.pixi${sep}envs`)) { result.type = "pixi"; result.pixi = { projectRoot: env.PIXI_PROJECT_ROOT || dirname(dirname(dirname(venvPath))), }; } else { result.type = env.CONDA_PREFIX?.includes("miniconda") || env.CONDA_PREFIX?.includes("mini") ? "miniconda" : "conda"; result.conda = { name: env.CONDA_DEFAULT_ENV || basename(venvPath), prefix: venvPath, }; } if (env.CONDA_VERSION) { result.toolVersion = env.CONDA_VERSION; } else { const historyPath = join(condaMetaDir, "history"); if (safeExistsSync(historyPath)) { const condaVersion = _extractCondaVersion(historyPath); if (condaVersion) result.toolVersion = condaVersion; } } const pythonPkgs = _findCondaPythonPackage(condaMetaDir); if (pythonPkgs?.version) { result.pythonVersion = pythonPkgs.version; } return result; } if (env.PYENV_ROOT && venvPath.startsWith(env.PYENV_ROOT)) { result.type = "pyenv"; const versionsDir = join(env.PYENV_ROOT, "versions"); if (venvPath.startsWith(`${versionsDir}${sep}`)) { const version = basename(venvPath); result.pyenv = { version, path: venvPath }; result.pythonVersion = version; } return result; } result.type = "unknown"; return result; } /** * Parse pyvenv.cfg file into key-value object */ function _parsePyvenvCfg(filePath) { const result = {}; try { const content = readFileSync(filePath, "utf-8"); for (const line of content.split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const match = trimmed.match(/^([^=]+?)\s*=\s*(.+)$/); if (match) { const [, key, value] = match; result[key.trim()] = value.trim(); } } } catch (_err) { // Return empty on error } return result; } /** * Extract poetry version from poetry.lock file */ function _extractPoetryVersion(lockPath) { try { const content = readFileSync(lockPath, "utf-8"); const match = content.match(/^\s*poetry-version\s*=\s*"([^"]+)"/m); return match ? match[1] : null; } catch (_err) { return null; } } /** * Extract conda version from conda-meta/history */ function _extractCondaVersion(historyPath) { try { const content = readFileSync(historyPath, "utf-8"); const match = content.match(/conda version:\s*(\S+)/i); return match ? match[1] : null; } catch (_err) { return null; } } /** * Find python package info in conda-meta directory */ function _findCondaPythonPackage(condaMetaDir) { try { const files = readdirSync(condaMetaDir); const pythonFile = files.find( (f) => f.startsWith("python-") && f.endsWith(".json"), ); if (!pythonFile) return null; const pkgInfo = JSON.parse( readFileSync(join(condaMetaDir, pythonFile), "utf-8"), ); return { version: pkgInfo?.version, build: pkgInfo?.build, }; } catch (_err) { return null; } } /** * Determines the appropriate Python executable path from a virtual environment. * Inspects the virtual environment metadata to detect the Python type (system, * conda, pyenv, etc.) and returns the most specific executable found, falling * back to the global `PYTHON_CMD` constant when no executable is detected. * * @param {string} env Path to the Python virtual environment directory * @returns {string} Path to the Python executable or the fallback command name */ export function get_python_command_from_env(env) { const fallbackCmd = PYTHON_CMD; const meta = getVenvMetadata(env); const pyVersionTxt = meta.pythonVersion && meta.pythonVersion !== "unknown" ? ` version ${meta.pythonVersion}` : ""; if (meta.type === "system") { thoughtLog( `I'm operating with system-managed python${pyVersionTxt}. I should be careful with the virtualenv creation and dependency tree construction.`, ); } else if (meta.type === "unknown") { thoughtLog( `I'm operating with an unmanaged python${pyVersionTxt}. Let's check if pip and virtualenv packages are available.`, ); } else { thoughtLog(`Looks like python${pyVersionTxt} is managed by ${meta.type}.`); } if (meta?.pythonExecutable) { if (DEBUG_MODE) { console.log( `Found python${pyVersionTxt} at ${meta.pythonExecutable} managed by ${meta.type}.`, ); } return meta.pythonExecutable; } return fallbackCmd; }