diff --git a/CHANGELOG.md b/CHANGELOG.md index 876012f2a..63ec626b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed - **Bazel diagnostics** — `socket manifest bazel --verbose` now emits bounded subprocess traces with argv, cwd, duration, exit status, output sizes, and failure stderr tails to make customer log-only triage safer and faster. +## [1.1.107](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.107) - 2026-05-28 + +### Changed +- **`socket manifest gradle --facts [beta]`** (and its `kotlin` alias) gained `--configs` and `--ignore-unresolved`, matching `socket manifest scala --facts`. `--configs` takes comma-separated glob patterns (e.g. `*CompileClasspath,*RuntimeClasspath`) to restrict resolution to matching Gradle configurations; unresolved dependencies are now a fatal error by default — pass `--ignore-unresolved` for the previous lenient behavior. +- **`socket manifest scala --facts --configs`** now accepts glob patterns too (e.g. `*Test*`) for consistency with the gradle command. Bare names (no `*`/`?`) keep working as exact-name filters, so existing usages are unchanged. + +### Fixed +- **`socket manifest gradle --facts`** now works on Gradle builds with the configuration cache enabled (default on Gradle 9), which previously failed with `Task.project at execution time` errors. + ## [1.1.106](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.106) - 2026-05-27 ### Added diff --git a/package.json b/package.json index 7d44ce600..6fd8c5311 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket", - "version": "1.1.106", + "version": "1.1.107", "description": "CLI for Socket.dev", "homepage": "https://github.com/SocketDev/socket-cli", "license": "MIT AND OFL-1.1", diff --git a/src/commands/manifest/cmd-manifest-gradle.mts b/src/commands/manifest/cmd-manifest-gradle.mts index 134c15d2e..62d665b56 100644 --- a/src/commands/manifest/cmd-manifest-gradle.mts +++ b/src/commands/manifest/cmd-manifest-gradle.mts @@ -34,6 +34,16 @@ const config: CliCommandConfig = { description: 'Emit a Socket facts JSON file (`.socket.facts.json`) describing the resolved dependency graph instead of generating `pom.xml` files', }, + configs: { + type: 'string', + description: + 'With --facts: comma-separated glob patterns matched against Gradle configuration names (case-sensitive, `*` and `?` wildcards). e.g. `*CompileClasspath,*RuntimeClasspath` to skip tooling configs. Default: every resolvable configuration except AGP instrumented-test classpaths', + }, + ignoreUnresolved: { + type: 'boolean', + description: + 'With --facts: warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file)', + }, gradleOpts: { type: 'string', description: @@ -69,11 +79,20 @@ const config: CliCommandConfig = { - it works with your \`gradlew\` from your repo and local settings and config + Pass --facts to instead emit a single \`.socket.facts.json\` describing the + resolved dependency graph of the whole build (no \`pom.xml\` files). An + unresolved dependency is a fatal error. With --facts you can pass + --configs= to restrict resolution to + matching configurations (e.g. \`*CompileClasspath,*RuntimeClasspath\`), + and --ignore-unresolved to warn on unresolved dependencies instead of + failing the run. + Support is beta. Please report issues or give us feedback on what's missing. Examples $ ${command} . + $ ${command} --facts . $ ${command} --bin=../gradlew . `, } @@ -116,7 +135,7 @@ async function run( sockJson?.defaults?.manifest?.gradle, ) - let { bin, facts, gradleOpts, verbose } = cli.flags + let { bin, configs, facts, gradleOpts, ignoreUnresolved, verbose } = cli.flags // Set defaults for any flag/arg that is not given. Check socket.json first. if (!bin) { @@ -154,6 +173,40 @@ async function run( facts = false } } + if (configs === undefined) { + if (sockJson.defaults?.manifest?.gradle?.configs !== undefined) { + configs = sockJson.defaults?.manifest?.gradle?.configs + logger.info(`Using default --configs from ${SOCKET_JSON}:`, configs) + } else { + configs = '' + } + } + if (ignoreUnresolved === undefined) { + if (sockJson.defaults?.manifest?.gradle?.ignoreUnresolved !== undefined) { + ignoreUnresolved = sockJson.defaults?.manifest?.gradle?.ignoreUnresolved + logger.info( + `Using default --ignore-unresolved from ${SOCKET_JSON}:`, + ignoreUnresolved, + ) + } else { + ignoreUnresolved = false + } + } + + // `--configs` and `--ignore-unresolved` only affect --facts; the pom path + // (the legacy `socketGenerateMaven` task) has no equivalent knobs. Warn + // rather than silently ignore an explicitly-passed flag. (socket.json + // defaults don't trip this — only a flag actually present on the command + // line does.) + if ( + !facts && + (cli.flags['configs'] !== undefined || + cli.flags['ignoreUnresolved'] !== undefined) + ) { + logger.warn( + 'The `--configs` and `--ignore-unresolved` options only apply with `--facts`; ignoring them.', + ) + } if (verbose) { logger.group('- ', parentName, config.commandName, ':') @@ -197,8 +250,10 @@ async function run( if (facts) { await convertGradleToFacts({ bin: String(bin), + configs: String(configs || ''), cwd, gradleOpts: parsedGradleOpts, + ignoreUnresolved: Boolean(ignoreUnresolved), verbose: Boolean(verbose), }) return diff --git a/src/commands/manifest/cmd-manifest-gradle.test.mts b/src/commands/manifest/cmd-manifest-gradle.test.mts index 8c5393441..6ccefeec1 100644 --- a/src/commands/manifest/cmd-manifest-gradle.test.mts +++ b/src/commands/manifest/cmd-manifest-gradle.test.mts @@ -24,8 +24,10 @@ describe('socket manifest gradle', async () => { Options --bin Location of gradlew binary to use, default: CWD/gradlew + --configs With --facts: comma-separated glob patterns matched against Gradle configuration names (case-sensitive, \`*\` and \`?\` wildcards). e.g. \`*CompileClasspath,*RuntimeClasspath\` to skip tooling configs. Default: every resolvable configuration except AGP instrumented-test classpaths --facts Emit a Socket facts JSON file (\`.socket.facts.json\`) describing the resolved dependency graph instead of generating \`pom.xml\` files --gradle-opts Additional options to pass on to ./gradlew, see \`./gradlew --help\` + --ignore-unresolved With --facts: warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file) --verbose Print debug messages Uses gradle, preferably through your local project \`gradlew\`, to generate a @@ -46,11 +48,20 @@ describe('socket manifest gradle', async () => { - it works with your \`gradlew\` from your repo and local settings and config + Pass --facts to instead emit a single \`.socket.facts.json\` describing the + resolved dependency graph of the whole build (no \`pom.xml\` files). An + unresolved dependency is a fatal error. With --facts you can pass + --configs= to restrict resolution to + matching configurations (e.g. \`*CompileClasspath,*RuntimeClasspath\`), + and --ignore-unresolved to warn on unresolved dependencies instead of + failing the run. + Support is beta. Please report issues or give us feedback on what's missing. Examples $ socket manifest gradle . + $ socket manifest gradle --facts . $ socket manifest gradle --bin=../gradlew ." `, ) diff --git a/src/commands/manifest/cmd-manifest-kotlin.mts b/src/commands/manifest/cmd-manifest-kotlin.mts index ffc3f227e..316b7d060 100644 --- a/src/commands/manifest/cmd-manifest-kotlin.mts +++ b/src/commands/manifest/cmd-manifest-kotlin.mts @@ -39,6 +39,16 @@ const config: CliCommandConfig = { description: 'Emit a Socket facts JSON file (`.socket.facts.json`) describing the resolved dependency graph instead of generating `pom.xml` files', }, + configs: { + type: 'string', + description: + 'With --facts: comma-separated glob patterns matched against Gradle configuration names (case-sensitive, `*` and `?` wildcards). e.g. `*CompileClasspath,*RuntimeClasspath` to skip tooling configs. Default: every resolvable configuration except AGP instrumented-test classpaths', + }, + ignoreUnresolved: { + type: 'boolean', + description: + 'With --facts: warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file)', + }, gradleOpts: { type: 'string', description: @@ -121,7 +131,7 @@ async function run( sockJson?.defaults?.manifest?.gradle, ) - let { bin, facts, gradleOpts, verbose } = cli.flags + let { bin, configs, facts, gradleOpts, ignoreUnresolved, verbose } = cli.flags // Set defaults for any flag/arg that is not given. Check socket.json first. if (!bin) { @@ -159,6 +169,35 @@ async function run( facts = false } } + if (configs === undefined) { + if (sockJson.defaults?.manifest?.gradle?.configs !== undefined) { + configs = sockJson.defaults?.manifest?.gradle?.configs + logger.info(`Using default --configs from ${SOCKET_JSON}:`, configs) + } else { + configs = '' + } + } + if (ignoreUnresolved === undefined) { + if (sockJson.defaults?.manifest?.gradle?.ignoreUnresolved !== undefined) { + ignoreUnresolved = sockJson.defaults?.manifest?.gradle?.ignoreUnresolved + logger.info( + `Using default --ignore-unresolved from ${SOCKET_JSON}:`, + ignoreUnresolved, + ) + } else { + ignoreUnresolved = false + } + } + + if ( + !facts && + (cli.flags['configs'] !== undefined || + cli.flags['ignoreUnresolved'] !== undefined) + ) { + logger.warn( + 'The `--configs` and `--ignore-unresolved` options only apply with `--facts`; ignoring them.', + ) + } if (verbose) { logger.group('- ', parentName, config.commandName, ':') @@ -202,8 +241,10 @@ async function run( if (facts) { await convertGradleToFacts({ bin: String(bin), + configs: String(configs || ''), cwd, gradleOpts: parsedGradleOpts, + ignoreUnresolved: Boolean(ignoreUnresolved), verbose: Boolean(verbose), }) return diff --git a/src/commands/manifest/cmd-manifest-kotlin.test.mts b/src/commands/manifest/cmd-manifest-kotlin.test.mts index ebb5d8510..d5d2112ad 100644 --- a/src/commands/manifest/cmd-manifest-kotlin.test.mts +++ b/src/commands/manifest/cmd-manifest-kotlin.test.mts @@ -24,8 +24,10 @@ describe('socket manifest kotlin', async () => { Options --bin Location of gradlew binary to use, default: CWD/gradlew + --configs With --facts: comma-separated glob patterns matched against Gradle configuration names (case-sensitive, \`*\` and \`?\` wildcards). e.g. \`*CompileClasspath,*RuntimeClasspath\` to skip tooling configs. Default: every resolvable configuration except AGP instrumented-test classpaths --facts Emit a Socket facts JSON file (\`.socket.facts.json\`) describing the resolved dependency graph instead of generating \`pom.xml\` files --gradle-opts Additional options to pass on to ./gradlew, see \`./gradlew --help\` + --ignore-unresolved With --facts: warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file) --verbose Print debug messages Uses gradle, preferably through your local project \`gradlew\`, to generate a diff --git a/src/commands/manifest/cmd-manifest-scala.mts b/src/commands/manifest/cmd-manifest-scala.mts index 155bfe775..24077f3d9 100644 --- a/src/commands/manifest/cmd-manifest-scala.mts +++ b/src/commands/manifest/cmd-manifest-scala.mts @@ -37,12 +37,12 @@ const config: CliCommandConfig = { configs: { type: 'string', description: - 'With --facts: comma-separated sbt configurations to resolve (default: compile,optional,provided,runtime,test)', + 'With --facts: comma-separated glob patterns matched against sbt configuration names (case-sensitive, `*` and `?` wildcards). Bare names (no wildcards) act as exact-name filters. Default: compile,optional,provided,runtime,test', }, ignoreUnresolved: { type: 'boolean', description: - 'With --facts: skip dependencies that fail to resolve instead of failing the run', + 'With --facts: warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file)', }, out: { type: 'string', @@ -95,8 +95,10 @@ const config: CliCommandConfig = { resolved dependency graph of the whole build (no \`pom.xml\` files). It reads dependency metadata only and never downloads artifacts; an unresolved dependency is a fatal error. With --facts you can pass - --configs=compile,test to choose which sbt configurations to resolve, and - --ignore-unresolved to skip dependencies that fail to resolve. + --configs= to choose which sbt configurations + to resolve (e.g. \`compile,test\` for exact names or \`*Test*\` for variants), + and --ignore-unresolved to warn on unresolved dependencies instead of + failing the run. Support is beta. Please report issues or give us feedback on what's missing. diff --git a/src/commands/manifest/cmd-manifest-scala.test.mts b/src/commands/manifest/cmd-manifest-scala.test.mts index e7067f250..9ed79fd64 100644 --- a/src/commands/manifest/cmd-manifest-scala.test.mts +++ b/src/commands/manifest/cmd-manifest-scala.test.mts @@ -24,9 +24,9 @@ describe('socket manifest scala', async () => { Options --bin Location of sbt binary to use - --configs With --facts: comma-separated sbt configurations to resolve (default: compile,optional,provided,runtime,test) + --configs With --facts: comma-separated glob patterns matched against sbt configuration names (case-sensitive, \`*\` and \`?\` wildcards). Bare names (no wildcards) act as exact-name filters. Default: compile,optional,provided,runtime,test --facts Emit a Socket facts JSON file (\`.socket.facts.json\`) describing the resolved dependency graph instead of generating \`pom.xml\` files - --ignore-unresolved With --facts: skip dependencies that fail to resolve instead of failing the run + --ignore-unresolved With --facts: warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file) --out Path of output file; where to store the resulting manifest, see also --stdout --sbt-opts Additional options to pass on to sbt, as per \`sbt --help\` --stdout Print resulting pom.xml to stdout (supersedes --out) @@ -58,8 +58,10 @@ describe('socket manifest scala', async () => { resolved dependency graph of the whole build (no \`pom.xml\` files). It reads dependency metadata only and never downloads artifacts; an unresolved dependency is a fatal error. With --facts you can pass - --configs=compile,test to choose which sbt configurations to resolve, and - --ignore-unresolved to skip dependencies that fail to resolve. + --configs= to choose which sbt configurations + to resolve (e.g. \`compile,test\` for exact names or \`*Test*\` for variants), + and --ignore-unresolved to warn on unresolved dependencies instead of + failing the run. Support is beta. Please report issues or give us feedback on what's missing. diff --git a/src/commands/manifest/convert-gradle-to-facts.mts b/src/commands/manifest/convert-gradle-to-facts.mts index 0efb23b66..8e4d3a37e 100644 --- a/src/commands/manifest/convert-gradle-to-facts.mts +++ b/src/commands/manifest/convert-gradle-to-facts.mts @@ -8,13 +8,17 @@ import constants from '../../constants.mts' export async function convertGradleToFacts({ bin, + configs, cwd, gradleOpts, + ignoreUnresolved, verbose, }: { bin: string + configs: string cwd: string gradleOpts: string[] + ignoreUnresolved: boolean verbose: boolean }): Promise { const rBin = path.resolve(cwd, bin) @@ -43,7 +47,34 @@ export async function convertGradleToFacts({ constants.distPath, 'socket-facts.init.gradle', ) + // Disable Gradle's configuration cache for the facts run. The init + // script resolves dependencies via the legacy + // `Configuration.resolvedConfiguration` API (the only public API that + // surfaces classifier + extension metadata) and registers per- + // subproject tasks that share a `gradle.ext` accumulator — neither + // pattern is compatible with the configuration cache, which would + // otherwise be on by default for projects with + // `org.gradle.configuration-cache=true` in `gradle.properties`. The + // Provider-based CC-safe alternatives (`ResolutionResult` / + // `ArtifactView.resolvedArtifacts`) only exist in Gradle 7.4+ and + // they don't expose classifier/extension, so they aren't a usable + // replacement here. Using `-D` rather than `--no-configuration-cache` + // keeps us compatible with older Gradle versions that don't recognize + // the flag — the system property is silently ignored when the + // feature doesn't exist. + // Both knobs are passed as Gradle project properties so the init script + // can read them via `rp.findProperty(...)`, matching how + // `socket.outputDirectory` / `socket.outputFile` are already wired. + const socketProps: string[] = [] + if (ignoreUnresolved) { + socketProps.push('-Psocket.ignoreUnresolved=true') + } + if (configs) { + socketProps.push(`-Psocket.configs=${configs}`) + } const commandArgs = [ + '-Dorg.gradle.configuration-cache=false', + ...socketProps, '--init-script', initLocation, ...gradleOpts, diff --git a/src/commands/manifest/generate_auto_manifest.mts b/src/commands/manifest/generate_auto_manifest.mts index 55c5fee24..43b89d62e 100644 --- a/src/commands/manifest/generate_auto_manifest.mts +++ b/src/commands/manifest/generate_auto_manifest.mts @@ -89,7 +89,13 @@ export async function generateAutoManifest({ logger.log( 'Detected a gradle build (Gradle, Kotlin, Scala), generating Socket facts...', ) - await convertGradleToFacts(gradleArgs) + await convertGradleToFacts({ + ...gradleArgs, + configs: sockJson.defaults?.manifest?.gradle?.configs ?? '', + ignoreUnresolved: Boolean( + sockJson.defaults?.manifest?.gradle?.ignoreUnresolved, + ), + }) } else { logger.log( 'Detected a gradle build (Gradle, Kotlin, Scala), running default gradle generator...', diff --git a/src/commands/manifest/socket-facts.init.gradle b/src/commands/manifest/socket-facts.init.gradle index 49ad5ba21..b9af15933 100644 --- a/src/commands/manifest/socket-facts.init.gradle +++ b/src/commands/manifest/socket-facts.init.gradle @@ -215,8 +215,50 @@ allprojects { project -> } def isTestConfig = { String name -> name.toLowerCase().contains('test') } + // Optional user-supplied filter: comma-separated glob patterns matched + // against full configuration names (case-sensitive — Gradle config + // names are canonical camelCase, and matching the user's literal input + // is more predictable than silently lower-casing). `*` matches any + // sequence of characters, `?` matches a single character. When set, + // only configurations whose name matches at least one pattern are + // walked. e.g. `--configs=*CompileClasspath,*RuntimeClasspath` keeps + // every variant of the standard classpath configs while filtering out + // tooling configs like `annotationProcessor` and + // `kotlinCompilerClasspath`. + def globToRegex = { String glob -> + def sb = new StringBuilder() + glob.each { String ch -> + switch (ch) { + case '*': sb << '.*'; break + case '?': sb << '.'; break + case '.': case '\\': case '^': case '$': case '|': + case '+': case '(': case ')': + case '[': case ']': case '{': case '}': + sb << '\\' << ch; break + default: sb << ch + } + } + java.util.regex.Pattern.compile(sb.toString()) + } + def configsProp = project.findProperty('socket.configs')?.toString() + def requestedPatterns = null + if (configsProp != null && !configsProp.trim().isEmpty()) { + requestedPatterns = configsProp.split(',') + .collect { it.trim() } + .findAll { !it.isEmpty() } + .collect { globToRegex(it) } + if (requestedPatterns.isEmpty()) { + requestedPatterns = null + } + } + def targetConfigs = project.configurations.findAll { - it.canBeResolved && !isAndroidInstrumentedTest(it.name) + if (!it.canBeResolved) return false + if (isAndroidInstrumentedTest(it.name)) return false + if (requestedPatterns != null) { + return requestedPatterns.any { p -> p.matcher(it.name).matches() } + } + return true } targetConfigs.each { cfg -> @@ -233,6 +275,13 @@ allprojects { project -> lenient.firstLevelModuleDependencies.each { dep -> directIds.addAll(visit(dep, isProd, isNonTooling, cache)) } + // Unresolved deps drive the abort/warn decision in the root + // aggregator but are deliberately NOT emitted as nodes — the + // selector-only coordinates (no classifier, no ext, possibly + // empty version) would surface as half-formed entries downstream + // and a consumer like coana's `mvn dependency:get` would try to + // fetch a phantom artifact. Matches the sbt plugin, whose + // `isEmittable` filter drops these the same way. lenient.unresolvedModuleDependencies.each { dep -> if (isIntraProject(dep.selector.group, dep.selector.name)) { return @@ -242,14 +291,6 @@ allprojects { project -> def reason = dep.problem?.message?.readLines()?.first() ?: 'unknown reason' println "[socket-facts] unresolved: ${selectorKey} in ${project.path}: ${reason}" } - def coord = [ - groupId : dep.selector.group ?: '', - artifactId: dep.selector.name, - version : dep.selector.version ?: '', - classifier: '', - ext : '', - ] - directIds.add(upsertNode(coord, isProd, isNonTooling)) } } catch (Exception e) { println "[socket-facts] skipping ${project.path}:${cfg.name}: ${e.message?.readLines()?.first()}" @@ -259,8 +300,25 @@ allprojects { project -> } } -rootProject { - tasks.create('socketFacts') { +rootProject { rp -> + // Capture project-derived values at configuration time so the task action + // doesn't reach back into `project` at execution time. Gradle's + // configuration cache forbids `Task.project` invocations from task + // actions; the Socket CLI disables the cache for the facts run via the + // `-Dorg.gradle.configuration-cache=false` flag, but hoisting these + // reads is cheap defensive code that keeps the script working even if + // a caller re-enables the cache by other means. + def outDirOverride = rp.findProperty('socket.outputDirectory')?.toString() + def outFileOverride = rp.findProperty('socket.outputFile')?.toString() + def defaultOutDir = rp.projectDir + def factsFileName = SOCKET_FACTS_FILENAME + // Unresolved dependencies are fatal by default — the user's environment is + // expected to resolve their declared deps. `-Psocket.ignoreUnresolved=true` + // (set by the CLI's `--ignore-unresolved`) downgrades that to a warning so + // the facts file still emits with whatever did resolve. + def ignoreUnresolved = rp.findProperty('socket.ignoreUnresolved')?.toString()?.toLowerCase() == 'true' + + rp.tasks.create('socketFacts') { group = 'socket' description = 'Aggregates a single Socket facts JSON for the entire build' outputs.upToDateWhen { false } @@ -269,6 +327,26 @@ rootProject { def state = gradle.socketFactsState def nodes = state.nodes def directIds = state.directIds + def reportedUnresolved = state.reportedUnresolved + + // Fail the build (or warn) once we've seen every collector's + // contributions. Subproject collectors already log each unresolved dep + // inline; this is the summary + decision point. + if (!reportedUnresolved.isEmpty()) { + def sorted = (reportedUnresolved as List).sort() + if (ignoreUnresolved) { + println "[socket-facts] ignoring ${sorted.size()} unresolved dependency(ies):" + sorted.each { println " - ${it}" } + } else { + println "[socket-facts] could not resolve ${sorted.size()} dependency(ies):" + sorted.each { println " - ${it}" } + throw new GradleException( + "Socket facts aborted: ${sorted.size()} unresolved dependency(ies). " + + "Pass --ignore-unresolved to skip them, or fix resolution (repositories, " + + "credentials, offline cache) and retry." + ) + } + } // Snapshot the accumulators under the same monitor used by writers in // each subproject's socketFactsCollect doLast. Task dependencies @@ -326,12 +404,10 @@ rootProject { return } - def outputDir = project.findProperty('socket.outputDirectory') - ? new File(project.findProperty('socket.outputDirectory').toString()) - : project.projectDir + def outputDir = outDirOverride ? new File(outDirOverride) : defaultOutDir outputDir.mkdirs() - def fileName = project.findProperty('socket.outputFile') ?: SOCKET_FACTS_FILENAME - def outFile = new File(outputDir, fileName.toString()) + def fileName = outFileOverride ?: factsFileName + def outFile = new File(outputDir, fileName) outFile.text = JsonOutput.prettyPrint(JsonOutput.toJson([components: components])) println "Socket facts file written to: ${outFile.absolutePath}" } diff --git a/src/commands/manifest/socket-facts.plugin.scala b/src/commands/manifest/socket-facts.plugin.scala index 2349ac508..ba2f50b7b 100644 --- a/src/commands/manifest/socket-facts.plugin.scala +++ b/src/commands/manifest/socket-facts.plugin.scala @@ -35,8 +35,10 @@ import scala.collection.mutable * universal, ...), `-internal` duplicates and the sources/docs/pom artifact * configs are skipped. They aren't the project's declared dependencies (the * pom-path manifest omits them too) and resolving them dominates cost on large - * builds. Override the set with `-Dsocket.configs=comma,separated` (e.g. - * `compile,test`, or add a custom config). One component is emitted per + * builds. Override the set with `-Dsocket.configs=comma,separated` glob + * patterns (case-sensitive; `*` = any sequence, `?` = single char). e.g. + * `compile,test` to keep only those scopes, `*Test*` to add custom test- + * like configs. One component is emitted per * resolved module (org:name:version); a module's alternate artifacts * (sources/javadoc classifier jars) are the same package, so they collapse * into that single component rather than adding duplicates. `test`-scoped @@ -160,15 +162,41 @@ object SocketFactsPlugin extends AutoPlugin { } ) - // The configurations to resolve: `-Dsocket.configs=a,b,c` if set, else the - // real dependency scopes. - private def requestedConfs: Set[String] = + // Build a name-matcher closed over `-Dsocket.configs`. When set, patterns + // are matched as case-sensitive globs (`*` = any sequence, `?` = single + // char) so the same flag shape works as on `socket manifest gradle + // --facts`. With no wildcards a pattern is just an exact-name match, + // which preserves the prior comma-separated-names semantics. When unset + // we fall back to exact membership in DefaultConfs. + private def buildConfigMatcher(): String => Boolean = sys.props.get("socket.configs") match { case Some(s) if s.trim.nonEmpty => - s.split(",").map(_.trim).filter(_.nonEmpty).toSet - case _ => DefaultConfs + val patterns = s + .split(",") + .map(_.trim) + .filter(_.nonEmpty) + .map(globToRegex) + .toList + if (patterns.isEmpty) { (name: String) => + DefaultConfs.contains(name) + } else { (name: String) => + patterns.exists(_.matcher(name).matches()) + } + case _ => (name: String) => DefaultConfs.contains(name) } + private def globToRegex(glob: String): java.util.regex.Pattern = { + val sb = new StringBuilder + glob.foreach { + case '*' => sb.append(".*") + case '?' => sb.append('.') + case c if "\\.^$|+()[]{}".indexOf(c.toInt) >= 0 => + sb.append('\\').append(c) + case c => sb.append(c) + } + java.util.regex.Pattern.compile(sb.toString) + } + private def boolProp(name: String): Boolean = java.lang.Boolean.parseBoolean(sys.props.getOrElse(name, "false")) @@ -183,8 +211,8 @@ object SocketFactsPlugin extends AutoPlugin { unresolved: mutable.LinkedHashSet[String] ): Unit = { val rootMrid = md.getModuleRevisionId - val wanted = requestedConfs - val confs = md.getConfigurationsNames.filter(wanted.contains) + val matcher = buildConfigMatcher() + val confs = md.getConfigurationsNames.filter(matcher) if (confs.nonEmpty) { // Don't revalidate cached metadata over the network: with release // coordinates the cached POM/ivy.xml never changes, so HEAD/GET-ing each diff --git a/src/utils/socket-json.mts b/src/utils/socket-json.mts index e6dd5abde..e6128ee61 100644 --- a/src/utils/socket-json.mts +++ b/src/utils/socket-json.mts @@ -61,8 +61,10 @@ export interface SocketJson { gradle?: { disabled?: boolean | undefined bin?: string | undefined + configs?: string | undefined facts?: boolean | undefined gradleOpts?: string | undefined + ignoreUnresolved?: boolean | undefined verbose?: boolean | undefined } sbt?: {