Skip to content
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
18 changes: 14 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import analysis from './analysis.js'
import fs from 'node:fs'
import { getCustom } from "./tools.js";
import { resolveBatchMetadata, resolveContinueOnError } from './batch_opts.js'
import { discoverMavenModules } from './providers/java_maven.js'
import {
discoverWorkspaceCrates,
discoverWorkspacePackages,
Expand All @@ -23,6 +24,7 @@ export { getProjectLicense, findLicenseFilePath, identifyLicense, getLicenseDeta

export default { componentAnalysis, stackAnalysis, stackAnalysisBatch, imageAnalysis, validateToken, generateSbom }
export {
discoverMavenModules,
discoverWorkspacePackages,
discoverWorkspaceCrates,
validatePackageJson,
Expand Down Expand Up @@ -98,7 +100,7 @@ export {
* @param {any} valueToBePrinted - The value to log.
* @private
*/
function logOptionsAndEnvironmentsVariables(alongsideText,valueToBePrinted) {
function logOptionsAndEnvironmentsVariables(alongsideText, valueToBePrinted) {
if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
console.log(`${alongsideText}: ${valueToBePrinted} ${EOL}`)
}
Expand All @@ -110,9 +112,9 @@ function logOptionsAndEnvironmentsVariables(alongsideText,valueToBePrinted) {
*/
function readAndPrintVersionFromPackageJson() {
let dirName
// new ESM way in nodeJS ( since node version 22 ) to bring module directory.
// new ESM way in nodeJS ( since node version 22 ) to bring module directory.
dirName = import.meta.dirname
// old ESM way in nodeJS ( before node versions 22.00 to bring module directory)
// old ESM way in nodeJS ( before node versions 22.00 to bring module directory)
if (!dirName) {
dirName = url.fileURLToPath(new URL('.', import.meta.url));
}
Expand Down Expand Up @@ -319,18 +321,26 @@ async function generateOneSbom(manifestPath, workspaceOpts) {
*
* @param {string} root - Resolved workspace root
* @param {Options} opts
* @returns {Promise<{ ecosystem: 'javascript' | 'cargo' | 'unknown', manifestPaths: string[] }>}
* @returns {Promise<{ ecosystem: 'javascript' | 'cargo' | 'maven' | 'unknown', manifestPaths: string[] }>}
* @private
*/
async function detectWorkspaceManifests(root, opts) {
const cargoToml = path.join(root, 'Cargo.toml')
const cargoLock = path.join(root, 'Cargo.lock')
const packageJson = path.join(root, 'package.json')
const pomXml = path.join(root, 'pom.xml')

if (fs.existsSync(cargoToml) && fs.existsSync(cargoLock)) {
return { ecosystem: 'cargo', manifestPaths: await discoverWorkspaceCrates(root, opts) }
}

if (fs.existsSync(pomXml)) {
const manifestPaths = await discoverMavenModules(root, opts)
if (manifestPaths.length > 0) {
return { ecosystem: 'maven', manifestPaths }
}
}

const hasJsLock = fs.existsSync(path.join(root, 'pnpm-lock.yaml'))
|| fs.existsSync(path.join(root, 'yarn.lock'))
|| fs.existsSync(path.join(root, 'package-lock.json'))
Expand Down
42 changes: 2 additions & 40 deletions src/providers/base_java.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import fs from 'node:fs'
import path from 'node:path'

import { PackageURL } from 'packageurl-js'

import { getCustomPath, getGitRootDir, getWrapperPreference, invokeCommand } from "../tools.js"
import { getCustomPath, getWrapperPreference, invokeCommand, traverseForWrapper } from "../tools.js"

/** @typedef {import('../provider').Provider} */

Expand Down Expand Up @@ -145,7 +144,7 @@ export default class Base_Java {

const useWrapper = getWrapperPreference(this.globalBinary, opts)
if (useWrapper) {
const wrapper = this.traverseForWrapper(manifestPath)
const wrapper = traverseForWrapper(manifestDir, this.localWrapper)
if (wrapper !== undefined) {
try {
this._invokeCommand(wrapper, ['--version'], {cwd: manifestDir})
Expand All @@ -168,41 +167,4 @@ export default class Base_Java {
return toolPath
}

/**
*
* @param {string} startingManifest - the path of the manifest from which to start searching for the wrapper
* @param {string} repoRoot - the root of the repository at which point to stop searching for mvnw, derived via git if unset and then fallsback
* to the root of the drive the manifest is on (assumes absolute path is given)
* @returns {string|undefined}
*/
traverseForWrapper(startingManifest, repoRoot = undefined) {
const normalizedManifest = this.normalizePath(startingManifest);
const currentDir = this.normalizePath(path.dirname(normalizedManifest));
repoRoot = repoRoot || getGitRootDir(currentDir) || path.parse(normalizedManifest).root;
const wrapperPath = path.join(currentDir, this.localWrapper);

try {
fs.accessSync(wrapperPath, fs.constants.X_OK);
return wrapperPath;
}
catch (error) {
if (error.code === 'ENOENT') {
const rootDir = path.parse(currentDir).root;
if (currentDir === repoRoot || currentDir === rootDir) {
return undefined;
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir || parentDir === rootDir) {
return undefined;
}
return this.traverseForWrapper(path.join(parentDir, path.basename(normalizedManifest)), repoRoot);
}
throw new Error(`failure searching for ${this.localWrapper}`, { cause: error });
}
}

normalizePath(thePath) {
const normalized = path.resolve(thePath).normalize();
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
}
}
101 changes: 100 additions & 1 deletion src/providers/java_maven.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { XMLParser } from 'fast-xml-parser'

import { getLicense } from '../license/license_utils.js'
import Sbom from '../sbom.js'
import { getCustom } from '../tools.js'
import { getCustom, invokeCommand } from '../tools.js'
import { filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore } from '../workspace.js'

import Base_java, { ecosystem_maven } from "./base_java.js";

Expand Down Expand Up @@ -309,3 +310,101 @@ export default class Java_maven extends Base_java {
return deps.filter(d => dep.artifactId === d.artifactId && dep.groupId === d.groupId && dep.scope === d.scope).length > 0
}
}

const DEFAULT_MAVEN_DISCOVERY_IGNORE = [
'**/target/**',
]

/**
* Discover all pom.xml manifest paths in a Maven multi-module project.
*
* @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain pom.xml)
* @param {object} [opts={}]
* @returns {Promise<string[]>} Paths to pom.xml files (absolute)
*/
export async function discoverMavenModules(workspaceRoot, opts = {}) {
const root = path.resolve(workspaceRoot)
const rootPom = path.join(root, 'pom.xml')

if (!fs.existsSync(rootPom)) {
return []
}

let mvnBin
try {
mvnBin = new Java_maven().selectToolBinary(rootPom, opts)
} catch {
return [rootPom]
}
const visited = new Set()
const manifestPaths = [rootPom]

collectMavenModules(root, mvnBin, visited, manifestPaths)

const ignorePatterns = [...resolveWorkspaceDiscoveryIgnore(opts), ...DEFAULT_MAVEN_DISCOVERY_IGNORE]
return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
}

/**
* @param {string} dir - Absolute path to directory containing pom.xml
* @param {string} mvnBin - Maven binary path
* @param {Set<string>} visited - Already-visited directories (cycle guard)
* @param {string[]} manifestPaths - Accumulator for discovered pom.xml paths
*/
function collectMavenModules(dir, mvnBin, visited, manifestPaths) {
const resolvedDir = path.resolve(dir)
if (visited.has(resolvedDir)) {
return
}
visited.add(resolvedDir)

const modules = listMavenModules(resolvedDir, mvnBin)
for (const mod of modules) {
const moduleDir = path.resolve(resolvedDir, mod)
const modulePom = path.join(moduleDir, 'pom.xml')
if (fs.existsSync(modulePom)) {
manifestPaths.push(modulePom)
collectMavenModules(moduleDir, mvnBin, visited, manifestPaths)
}
}
}

/**
* @param {string} dir - Directory containing pom.xml
* @param {string} mvnBin - Maven binary path
* @returns {string[]} Module directory names (relative to `dir`)
*/
function listMavenModules(dir, mvnBin) {
let output
try {
output = invokeCommand(mvnBin, [
'help:evaluate',
'-Dexpression=project.modules',
'-q',
'-DforceStdout',
'-f', path.join(dir, 'pom.xml'),
'--batch-mode',
], { cwd: dir })
} catch {
return []
}

const raw = output.toString().trim()
if (!raw || raw.startsWith('<modules')) {
return []
}
return parseMavenModuleList(raw)
}

/**
* @param {string} raw - Raw stdout from mvn help:evaluate -DforceStdout
* @returns {string[]}
*/
function parseMavenModuleList(raw) {
const parser = new XMLParser()
const parsed = parser.parse(raw)
const entries = parsed?.strings?.string
if (!entries) { return [] }
const list = Array.isArray(entries) ? entries : [entries]
return list.map(s => String(s).trim()).filter(Boolean)
}
41 changes: 41 additions & 0 deletions src/tools.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { execFileSync } from "child_process";
import fs from 'node:fs'
import path from 'node:path'
import { EOL } from "os";

import { HttpsProxyAgent } from "https-proxy-agent";
Expand Down Expand Up @@ -137,6 +139,45 @@ export function getGitRootDir(cwd) {
}
}

/**
* Normalize a filesystem path, lowercasing on Windows for case-insensitive comparison.
*
* @param {string} thePath
* @returns {string}
*/
export function normalizePath(thePath) {
const normalized = path.resolve(thePath).normalize()
return process.platform === 'win32' ? normalized.toLowerCase() : normalized
}

/**
* Walk up from `startDir` to `repoRoot` looking for an executable wrapper script.
*
* @param {string} startDir - Absolute directory to start from
* @param {string} wrapperName - Wrapper filename (e.g. `mvnw`, `gradlew`)
* @param {string} [repoRoot] - Stop boundary (defaults to git root or filesystem root)
* @returns {string | undefined}
*/
export function traverseForWrapper(startDir, wrapperName, repoRoot = undefined) {
Comment thread
Strum355 marked this conversation as resolved.
const currentDir = normalizePath(startDir)
repoRoot = repoRoot || getGitRootDir(currentDir) || path.parse(currentDir).root
const wrapperPath = path.join(currentDir, wrapperName)
try {
fs.accessSync(wrapperPath, fs.constants.X_OK)
return wrapperPath
} catch {
const rootDir = path.parse(currentDir).root
if (currentDir === repoRoot || currentDir === rootDir) {
return undefined
}
const parentDir = path.dirname(currentDir)
if (parentDir === currentDir || parentDir === rootDir) {
return undefined
}
return traverseForWrapper(parentDir, wrapperName, repoRoot)
}
}

/** this method invokes command string in a process in a synchronous way.
* @param {string} bin - the command to be invoked
* @param {Array<string>} args - the args to pass to the binary
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>module-a</artifactId>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>module-b</artifactId>
</project>
11 changes: 11 additions & 0 deletions test/providers/tst_manifests/maven/maven_multi_module/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>module-a</module>
<module>module-b</module>
</modules>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>child</artifactId>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>child</module>
</modules>
</project>
10 changes: 10 additions & 0 deletions test/providers/tst_manifests/maven/maven_nested_aggregator/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>root</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>parent</module>
</modules>
</project>
6 changes: 6 additions & 0 deletions test/providers/tst_manifests/maven/maven_no_modules/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>single</artifactId>
<version>1.0.0</version>
</project>
Loading
Loading