From 43839422eae8cb910eec1f3f72d4809c07d3504a Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Tue, 2 Jun 2026 11:45:25 +0200 Subject: [PATCH 1/5] feat(manifest): default to Socket facts, delegate generation to Coana CLI Make `socket manifest {gradle,kotlin,scala,auto}` emit `.socket.facts.json` by default; add `--pom` for the legacy pom.xml generation. `--facts` is still accepted (it is now the default) and socket.json `facts: false` still selects pom generation. Facts generation now delegates to the Coana CLI's `manifest gradle|sbt` command (the Gradle init script and sbt plugin live in Coana now), forwarding --bin/--configs/--ignore-unresolved/--gradle-opts/--sbt-opts via spawnCoanaDlx (which honors SOCKET_CLI_COANA_LOCAL_PATH for local builds). Remove the now-dead bundled socket-facts.init.gradle, the sbt plugin, their rollup copy steps, and the ported gradle-facts test fixtures. The pom-path init.gradle is unchanged. REA-507 --- .config/rollup.dist.config.mjs | 20 - src/commands/manifest/cmd-manifest-gradle.mts | 56 ++- .../manifest/cmd-manifest-gradle.test.mts | 40 +- src/commands/manifest/cmd-manifest-kotlin.mts | 47 +- .../manifest/cmd-manifest-kotlin.test.mts | 31 +- src/commands/manifest/cmd-manifest-scala.mts | 47 +- .../manifest/cmd-manifest-scala.test.mts | 31 +- src/commands/manifest/cmd-manifest.test.mts | 6 +- .../manifest/coana-manifest-facts.mts | 85 ++++ .../manifest/convert-gradle-to-facts.mts | 186 +------- .../manifest/convert-sbt-to-facts.mts | 233 +--------- .../manifest/generate_auto_manifest.mts | 8 +- .../manifest/generate_auto_manifest.test.mts | 24 + .../manifest/socket-facts.init.gradle | 429 ------------------ .../manifest/socket-facts.plugin.scala | 416 ----------------- src/constants.mts | 6 +- .../commands/manifest/gradle-facts/.gitignore | 5 - .../gradle-facts/android-library/build.gradle | 32 -- .../android-library/gradle.properties | 1 - .../android-library/settings.gradle | 16 - .../kotlin-multiplatform/build.gradle | 37 -- .../kotlin-multiplatform/settings.gradle | 9 - .../multi-module-java/app/build.gradle | 8 - .../multi-module-java/build.gradle | 8 - .../multi-module-java/lib/build.gradle | 8 - .../multi-module-java/settings.gradle | 4 - .../single-module-java/build.gradle | 25 - .../single-module-java/settings.gradle | 1 - .../gradle-facts/unresolved-deps/build.gradle | 20 - .../unresolved-deps/settings.gradle | 1 - 30 files changed, 301 insertions(+), 1539 deletions(-) create mode 100644 src/commands/manifest/coana-manifest-facts.mts delete mode 100644 src/commands/manifest/socket-facts.init.gradle delete mode 100644 src/commands/manifest/socket-facts.plugin.scala delete mode 100644 test/fixtures/commands/manifest/gradle-facts/.gitignore delete mode 100644 test/fixtures/commands/manifest/gradle-facts/android-library/build.gradle delete mode 100644 test/fixtures/commands/manifest/gradle-facts/android-library/gradle.properties delete mode 100644 test/fixtures/commands/manifest/gradle-facts/android-library/settings.gradle delete mode 100644 test/fixtures/commands/manifest/gradle-facts/kotlin-multiplatform/build.gradle delete mode 100644 test/fixtures/commands/manifest/gradle-facts/kotlin-multiplatform/settings.gradle delete mode 100644 test/fixtures/commands/manifest/gradle-facts/multi-module-java/app/build.gradle delete mode 100644 test/fixtures/commands/manifest/gradle-facts/multi-module-java/build.gradle delete mode 100644 test/fixtures/commands/manifest/gradle-facts/multi-module-java/lib/build.gradle delete mode 100644 test/fixtures/commands/manifest/gradle-facts/multi-module-java/settings.gradle delete mode 100644 test/fixtures/commands/manifest/gradle-facts/single-module-java/build.gradle delete mode 100644 test/fixtures/commands/manifest/gradle-facts/single-module-java/settings.gradle delete mode 100644 test/fixtures/commands/manifest/gradle-facts/unresolved-deps/build.gradle delete mode 100644 test/fixtures/commands/manifest/gradle-facts/unresolved-deps/settings.gradle diff --git a/.config/rollup.dist.config.mjs b/.config/rollup.dist.config.mjs index 7a60c7f34..7857466ad 100644 --- a/.config/rollup.dist.config.mjs +++ b/.config/rollup.dist.config.mjs @@ -79,24 +79,6 @@ async function copyInitGradle() { await fs.copyFile(filepath, destPath) } -async function copySocketFactsInitGradle() { - const filepath = path.join( - constants.srcPath, - 'commands/manifest/socket-facts.init.gradle', - ) - const destPath = path.join(constants.distPath, 'socket-facts.init.gradle') - await fs.copyFile(filepath, destPath) -} - -async function copySocketFactsSbtPlugin() { - const filepath = path.join( - constants.srcPath, - 'commands/manifest/socket-facts.plugin.scala', - ) - const destPath = path.join(constants.distPath, 'socket-facts.plugin.scala') - await fs.copyFile(filepath, destPath) -} - async function copyBashCompletion() { const filepath = path.join( constants.srcPath, @@ -476,8 +458,6 @@ export default async () => { async writeBundle() { await Promise.all([ copyInitGradle(), - copySocketFactsInitGradle(), - copySocketFactsSbtPlugin(), copyBashCompletion(), updatePackageJson(), // Remove dist/vendor.js.map file. diff --git a/src/commands/manifest/cmd-manifest-gradle.mts b/src/commands/manifest/cmd-manifest-gradle.mts index 62d665b56..43b2cb96f 100644 --- a/src/commands/manifest/cmd-manifest-gradle.mts +++ b/src/commands/manifest/cmd-manifest-gradle.mts @@ -21,7 +21,7 @@ import type { const config: CliCommandConfig = { commandName: 'gradle', description: - '[beta] Use Gradle to generate a manifest file (`pom.xml`) for a Gradle/Java/Kotlin/etc project', + '[beta] Generate a Socket facts file (or `pom.xml` with --pom) for a Gradle/Java/Kotlin/etc project', hidden: false, flags: { ...commonFlags, @@ -32,7 +32,12 @@ const config: CliCommandConfig = { facts: { type: 'boolean', description: - 'Emit a Socket facts JSON file (`.socket.facts.json`) describing the resolved dependency graph instead of generating `pom.xml` files', + 'Emit a Socket facts JSON file (`.socket.facts.json`) describing the resolved dependency graph. This is the default; pass `--pom` to generate `pom.xml` files instead', + }, + pom: { + type: 'boolean', + description: + 'Generate `pom.xml` manifest file(s) instead of the default Socket facts file (`.socket.facts.json`)', }, configs: { type: 'string', @@ -61,38 +66,31 @@ const config: CliCommandConfig = { Options ${getFlagListOutput(config.flags)} - Uses gradle, preferably through your local project \`gradlew\`, to generate a - \`pom.xml\` file for each task. If you have no \`gradlew\` you can try the - global \`gradle\` binary but that may not work (hard to predict). + By default, emits a single \`.socket.facts.json\` describing the resolved + dependency graph of the whole build, using gradle (preferably your local + \`gradlew\`). An unresolved dependency is a fatal error. 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 \`pom.xml\` is a manifest file similar to \`package.json\` for npm or - or ${REQUIREMENTS_TXT} for PyPi), but specifically for Maven, which is Java's - dependency repository. Languages like Kotlin and Scala piggy back on it too. + Pass --pom to instead generate \`pom.xml\` manifest files via gradle (one per + task). The \`pom.xml\` is a manifest file similar to \`package.json\` for npm + (or ${REQUIREMENTS_TXT} for PyPi), but specifically for Maven, which is + Java's dependency repository. Caveats of the \`pom.xml\` conversion: - There are some caveats with the gradle to \`pom.xml\` conversion: + - each task generates its own xml file (one per task by default) - - each task will generate its own xml file and by default it generates one xml - for every task. (This may be a good thing!) - - - it's possible certain features don't translate well into the xml. If you - think something is missing that could be supported please reach out. + - certain features may not translate well into the xml; reach out if + something you need is missing - 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} --pom . $ ${command} --bin=../gradlew . `, } @@ -169,6 +167,18 @@ async function run( if (sockJson.defaults?.manifest?.gradle?.facts !== undefined) { facts = sockJson.defaults?.manifest?.gradle?.facts logger.info(`Using default --facts from ${SOCKET_JSON}:`, facts) + } else { + // Socket facts generation is the default; pass --pom to generate poms. + facts = true + } + } + // --pom opts into legacy pom.xml generation. It overrides the facts default + // (and the socket.json default) but conflicts with an explicit --facts. + if (cli.flags['pom']) { + if (cli.flags['facts'] !== undefined) { + logger.warn( + 'The `--facts` and `--pom` options are mutually exclusive; generating Socket facts.', + ) } else { facts = false } diff --git a/src/commands/manifest/cmd-manifest-gradle.test.mts b/src/commands/manifest/cmd-manifest-gradle.test.mts index 6ccefeec1..dd5868027 100644 --- a/src/commands/manifest/cmd-manifest-gradle.test.mts +++ b/src/commands/manifest/cmd-manifest-gradle.test.mts @@ -17,7 +17,7 @@ describe('socket manifest gradle', async () => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) expect(stdout).toMatchInlineSnapshot( ` - "[beta] Use Gradle to generate a manifest file (\`pom.xml\`) for a Gradle/Java/Kotlin/etc project + "[beta] Generate a Socket facts file (or \`pom.xml\` with --pom) for a Gradle/Java/Kotlin/etc project Usage $ socket manifest gradle [options] [CWD=.] @@ -25,43 +25,37 @@ 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 + --facts Emit a Socket facts JSON file (\`.socket.facts.json\`) describing the resolved dependency graph. This is the default; pass \`--pom\` to generate \`pom.xml\` files instead --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) + --pom Generate \`pom.xml\` manifest file(s) instead of the default Socket facts file (\`.socket.facts.json\`) --verbose Print debug messages - Uses gradle, preferably through your local project \`gradlew\`, to generate a - \`pom.xml\` file for each task. If you have no \`gradlew\` you can try the - global \`gradle\` binary but that may not work (hard to predict). + By default, emits a single \`.socket.facts.json\` describing the resolved + dependency graph of the whole build, using gradle (preferably your local + \`gradlew\`). An unresolved dependency is a fatal error. 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 \`pom.xml\` is a manifest file similar to \`package.json\` for npm or - or requirements.txt for PyPi), but specifically for Maven, which is Java's - dependency repository. Languages like Kotlin and Scala piggy back on it too. + Pass --pom to instead generate \`pom.xml\` manifest files via gradle (one per + task). The \`pom.xml\` is a manifest file similar to \`package.json\` for npm + (or requirements.txt for PyPi), but specifically for Maven, which is + Java's dependency repository. Caveats of the \`pom.xml\` conversion: - There are some caveats with the gradle to \`pom.xml\` conversion: + - each task generates its own xml file (one per task by default) - - each task will generate its own xml file and by default it generates one xml - for every task. (This may be a good thing!) - - - it's possible certain features don't translate well into the xml. If you - think something is missing that could be supported please reach out. + - certain features may not translate well into the xml; reach out if + something you need is missing - 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 --pom . $ socket manifest gradle --bin=../gradlew ." `, ) diff --git a/src/commands/manifest/cmd-manifest-kotlin.mts b/src/commands/manifest/cmd-manifest-kotlin.mts index 316b7d060..1411b01bb 100644 --- a/src/commands/manifest/cmd-manifest-kotlin.mts +++ b/src/commands/manifest/cmd-manifest-kotlin.mts @@ -26,7 +26,7 @@ import type { const config: CliCommandConfig = { commandName: 'kotlin', description: - '[beta] Use Gradle to generate a manifest file (`pom.xml`) for a Kotlin project', + '[beta] Generate a Socket facts file (or `pom.xml` with --pom) for a Kotlin project', hidden: false, flags: { ...commonFlags, @@ -37,7 +37,12 @@ const config: CliCommandConfig = { facts: { type: 'boolean', description: - 'Emit a Socket facts JSON file (`.socket.facts.json`) describing the resolved dependency graph instead of generating `pom.xml` files', + 'Emit a Socket facts JSON file (`.socket.facts.json`) describing the resolved dependency graph. This is the default; pass `--pom` to generate `pom.xml` files instead', + }, + pom: { + type: 'boolean', + description: + 'Generate `pom.xml` manifest file(s) instead of the default Socket facts file (`.socket.facts.json`)', }, configs: { type: 'string', @@ -66,21 +71,22 @@ const config: CliCommandConfig = { Options ${getFlagListOutput(config.flags)} - Uses gradle, preferably through your local project \`gradlew\`, to generate a - \`pom.xml\` file for each task. If you have no \`gradlew\` you can try the - global \`gradle\` binary but that may not work (hard to predict). - - The \`pom.xml\` is a manifest file similar to \`package.json\` for npm or - or ${REQUIREMENTS_TXT} for PyPi), but specifically for Maven, which is Java's - dependency repository. Languages like Kotlin and Scala piggy back on it too. + By default, emits a single \`.socket.facts.json\` describing the resolved + dependency graph of the whole build, using gradle (preferably your local + \`gradlew\`). An unresolved dependency is a fatal error. 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. - There are some caveats with the gradle to \`pom.xml\` conversion: + Pass --pom to instead generate \`pom.xml\` manifest files via gradle (one per + task). The \`pom.xml\` is a manifest file similar to \`package.json\` for npm + (or ${REQUIREMENTS_TXT} for PyPi), but specifically for Maven, which is + Java's dependency repository. Caveats of the \`pom.xml\` conversion: - - each task will generate its own xml file and by default it generates one xml - for every task. (This may be a good thing!) + - each task generates its own xml file (one per task by default) - - it's possible certain features don't translate well into the xml. If you - think something is missing that could be supported please reach out. + - certain features may not translate well into the xml; reach out if + something you need is missing - it works with your \`gradlew\` from your repo and local settings and config @@ -89,6 +95,7 @@ const config: CliCommandConfig = { Examples $ ${command} . + $ ${command} --pom . $ ${command} --bin=../gradlew . `, } @@ -165,6 +172,18 @@ async function run( if (sockJson.defaults?.manifest?.gradle?.facts !== undefined) { facts = sockJson.defaults?.manifest?.gradle?.facts logger.info(`Using default --facts from ${SOCKET_JSON}:`, facts) + } else { + // Socket facts generation is the default; pass --pom to generate poms. + facts = true + } + } + // --pom opts into legacy pom.xml generation. It overrides the facts default + // (and the socket.json default) but conflicts with an explicit --facts. + if (cli.flags['pom']) { + if (cli.flags['facts'] !== undefined) { + logger.warn( + 'The `--facts` and `--pom` options are mutually exclusive; generating Socket facts.', + ) } else { facts = false } diff --git a/src/commands/manifest/cmd-manifest-kotlin.test.mts b/src/commands/manifest/cmd-manifest-kotlin.test.mts index d5d2112ad..11c3384b6 100644 --- a/src/commands/manifest/cmd-manifest-kotlin.test.mts +++ b/src/commands/manifest/cmd-manifest-kotlin.test.mts @@ -17,7 +17,7 @@ describe('socket manifest kotlin', async () => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) expect(stdout).toMatchInlineSnapshot( ` - "[beta] Use Gradle to generate a manifest file (\`pom.xml\`) for a Kotlin project + "[beta] Generate a Socket facts file (or \`pom.xml\` with --pom) for a Kotlin project Usage $ socket manifest kotlin [options] [CWD=.] @@ -25,26 +25,28 @@ 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 + --facts Emit a Socket facts JSON file (\`.socket.facts.json\`) describing the resolved dependency graph. This is the default; pass \`--pom\` to generate \`pom.xml\` files instead --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) + --pom Generate \`pom.xml\` manifest file(s) instead of the default Socket facts file (\`.socket.facts.json\`) --verbose Print debug messages - Uses gradle, preferably through your local project \`gradlew\`, to generate a - \`pom.xml\` file for each task. If you have no \`gradlew\` you can try the - global \`gradle\` binary but that may not work (hard to predict). + By default, emits a single \`.socket.facts.json\` describing the resolved + dependency graph of the whole build, using gradle (preferably your local + \`gradlew\`). An unresolved dependency is a fatal error. 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 \`pom.xml\` is a manifest file similar to \`package.json\` for npm or - or requirements.txt for PyPi), but specifically for Maven, which is Java's - dependency repository. Languages like Kotlin and Scala piggy back on it too. + Pass --pom to instead generate \`pom.xml\` manifest files via gradle (one per + task). The \`pom.xml\` is a manifest file similar to \`package.json\` for npm + (or requirements.txt for PyPi), but specifically for Maven, which is + Java's dependency repository. Caveats of the \`pom.xml\` conversion: - There are some caveats with the gradle to \`pom.xml\` conversion: + - each task generates its own xml file (one per task by default) - - each task will generate its own xml file and by default it generates one xml - for every task. (This may be a good thing!) - - - it's possible certain features don't translate well into the xml. If you - think something is missing that could be supported please reach out. + - certain features may not translate well into the xml; reach out if + something you need is missing - it works with your \`gradlew\` from your repo and local settings and config @@ -53,6 +55,7 @@ describe('socket manifest kotlin', async () => { Examples $ socket manifest kotlin . + $ socket manifest kotlin --pom . $ socket manifest kotlin --bin=../gradlew ." `, ) diff --git a/src/commands/manifest/cmd-manifest-scala.mts b/src/commands/manifest/cmd-manifest-scala.mts index 24077f3d9..faffa6e2e 100644 --- a/src/commands/manifest/cmd-manifest-scala.mts +++ b/src/commands/manifest/cmd-manifest-scala.mts @@ -21,7 +21,7 @@ import type { const config: CliCommandConfig = { commandName: 'scala', description: - "[beta] Generate a manifest file (`pom.xml`) from Scala's `build.sbt` file", + '[beta] Generate a Socket facts file (or `pom.xml` with --pom) from a Scala `build.sbt` project', hidden: false, flags: { ...commonFlags, @@ -32,7 +32,12 @@ const config: CliCommandConfig = { facts: { type: 'boolean', description: - 'Emit a Socket facts JSON file (`.socket.facts.json`) describing the resolved dependency graph instead of generating `pom.xml` files', + 'Emit a Socket facts JSON file (`.socket.facts.json`) describing the resolved dependency graph. This is the default; pass `--pom` to generate `pom.xml` files instead', + }, + pom: { + type: 'boolean', + description: + 'Generate `pom.xml` manifest file(s) instead of the default Socket facts file (`.socket.facts.json`)', }, configs: { type: 'string', @@ -69,11 +74,18 @@ const config: CliCommandConfig = { Options ${getFlagListOutput(config.flags)} - Uses \`sbt makePom\` to generate a \`pom.xml\` from your \`build.sbt\` file. - This xml file is the dependency manifest (like a package.json - for Node.js or ${REQUIREMENTS_TXT} for PyPi), but specifically for Scala. + By default, emits a single \`.socket.facts.json\` describing the resolved + dependency graph of the whole build. It reads dependency metadata only and + never downloads artifacts; an unresolved dependency is a fatal error. You + can pass --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. - There are some caveats with \`build.sbt\` to \`pom.xml\` conversion: + Pass --pom to instead generate a \`pom.xml\` via \`sbt makePom\` from your + \`build.sbt\`. The xml is the dependency manifest (like a package.json for + Node.js or ${REQUIREMENTS_TXT} for PyPi), but specifically for Scala. + Caveats of the \`build.sbt\` to \`pom.xml\` conversion: - the xml is exported as pom.xml at the project root so Socket scan picks it up; sbt itself first writes it inside your /target/sbt folder @@ -91,15 +103,6 @@ const config: CliCommandConfig = { You can specify --bin to override the path to the \`sbt\` binary to invoke. - Pass --facts to instead emit a single \`.socket.facts.json\` describing the - 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= 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. This is only for SBT. If your Scala setup uses gradle, please see the help @@ -108,7 +111,7 @@ const config: CliCommandConfig = { Examples $ ${command} - $ ${command} --facts . + $ ${command} --pom . $ ${command} ./proj --bin=/usr/bin/sbt --file=boot.sbt `, } @@ -167,6 +170,18 @@ async function run( if (sockJson.defaults?.manifest?.sbt?.facts !== undefined) { facts = sockJson.defaults?.manifest?.sbt?.facts logger.info(`Using default --facts from ${SOCKET_JSON}:`, facts) + } else { + // Socket facts generation is the default; pass --pom to generate poms. + facts = true + } + } + // --pom opts into legacy pom.xml generation. It overrides the facts default + // (and the socket.json default) but conflicts with an explicit --facts. + if (cli.flags['pom']) { + if (cli.flags['facts'] !== undefined) { + logger.warn( + 'The `--facts` and `--pom` options are mutually exclusive; generating Socket facts.', + ) } else { facts = false } diff --git a/src/commands/manifest/cmd-manifest-scala.test.mts b/src/commands/manifest/cmd-manifest-scala.test.mts index 9ed79fd64..c647265d1 100644 --- a/src/commands/manifest/cmd-manifest-scala.test.mts +++ b/src/commands/manifest/cmd-manifest-scala.test.mts @@ -17,7 +17,7 @@ describe('socket manifest scala', async () => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) expect(stdout).toMatchInlineSnapshot( ` - "[beta] Generate a manifest file (\`pom.xml\`) from Scala's \`build.sbt\` file + "[beta] Generate a Socket facts file (or \`pom.xml\` with --pom) from a Scala \`build.sbt\` project Usage $ socket manifest scala [options] [CWD=.] @@ -25,18 +25,26 @@ describe('socket manifest scala', async () => { Options --bin Location of sbt binary to use --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 + --facts Emit a Socket facts JSON file (\`.socket.facts.json\`) describing the resolved dependency graph. This is the default; pass \`--pom\` to generate \`pom.xml\` files instead --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 + --pom Generate \`pom.xml\` manifest file(s) instead of the default Socket facts file (\`.socket.facts.json\`) --sbt-opts Additional options to pass on to sbt, as per \`sbt --help\` --stdout Print resulting pom.xml to stdout (supersedes --out) --verbose Print debug messages - Uses \`sbt makePom\` to generate a \`pom.xml\` from your \`build.sbt\` file. - This xml file is the dependency manifest (like a package.json - for Node.js or requirements.txt for PyPi), but specifically for Scala. + By default, emits a single \`.socket.facts.json\` describing the resolved + dependency graph of the whole build. It reads dependency metadata only and + never downloads artifacts; an unresolved dependency is a fatal error. You + can pass --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. - There are some caveats with \`build.sbt\` to \`pom.xml\` conversion: + Pass --pom to instead generate a \`pom.xml\` via \`sbt makePom\` from your + \`build.sbt\`. The xml is the dependency manifest (like a package.json for + Node.js or requirements.txt for PyPi), but specifically for Scala. + Caveats of the \`build.sbt\` to \`pom.xml\` conversion: - the xml is exported as pom.xml at the project root so Socket scan picks it up; sbt itself first writes it inside your /target/sbt folder @@ -54,15 +62,6 @@ describe('socket manifest scala', async () => { You can specify --bin to override the path to the \`sbt\` binary to invoke. - Pass --facts to instead emit a single \`.socket.facts.json\` describing the - 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= 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. This is only for SBT. If your Scala setup uses gradle, please see the help @@ -71,7 +70,7 @@ describe('socket manifest scala', async () => { Examples $ socket manifest scala - $ socket manifest scala --facts . + $ socket manifest scala --pom . $ socket manifest scala ./proj --bin=/usr/bin/sbt --file=boot.sbt" `, ) diff --git a/src/commands/manifest/cmd-manifest.test.mts b/src/commands/manifest/cmd-manifest.test.mts index 5f3504d00..604eb451c 100644 --- a/src/commands/manifest/cmd-manifest.test.mts +++ b/src/commands/manifest/cmd-manifest.test.mts @@ -27,9 +27,9 @@ describe('socket manifest', async () => { bazel [beta] Bazel SBOM support \\u2014 generate manifest files for a Bazel project (Maven, PyPI) cdxgen Run cdxgen for SBOM generation conda [beta] Convert a Conda environment.yml file to a python requirements.txt - gradle [beta] Use Gradle to generate a manifest file (\`pom.xml\`) for a Gradle/Java/Kotlin/etc project - kotlin [beta] Use Gradle to generate a manifest file (\`pom.xml\`) for a Kotlin project - scala [beta] Generate a manifest file (\`pom.xml\`) from Scala's \`build.sbt\` file + gradle [beta] Generate a Socket facts file (or \`pom.xml\` with --pom) for a Gradle/Java/Kotlin/etc project + kotlin [beta] Generate a Socket facts file (or \`pom.xml\` with --pom) for a Kotlin project + scala [beta] Generate a Socket facts file (or \`pom.xml\` with --pom) from a Scala \`build.sbt\` project setup Start interactive configurator to customize default flag values for \`socket manifest\` in this dir Options diff --git a/src/commands/manifest/coana-manifest-facts.mts b/src/commands/manifest/coana-manifest-facts.mts new file mode 100644 index 000000000..0813a8383 --- /dev/null +++ b/src/commands/manifest/coana-manifest-facts.mts @@ -0,0 +1,85 @@ +import { logger } from '@socketsecurity/registry/lib/logger' + +import { spawnCoanaDlx } from '../../utils/dlx.mts' + +// Delegates Socket facts generation for a JVM build tool to the Coana CLI's +// `manifest ` command. The build-tool resolution scripts (the Gradle +// init script and the sbt plugin) live in Coana now, so socket-cli no longer +// runs them itself; it only asks Coana for the uploadable `.socket.facts.json`. +// +// The resolved artifact-paths sidecar is intentionally NOT requested here: it +// only matters for reachability analysis, which is internal to Coana, so Coana +// emits it itself when it runs reachability. `socket manifest` only needs the +// facts file. +// +// `spawnCoanaDlx` resolves the Coana CLI via dlx (or a local build when +// `SOCKET_CLI_COANA_LOCAL_PATH` is set). `bin` (the gradle/sbt executable) is +// forwarded only when explicitly chosen; when empty, Coana applies the same +// default socket-cli used to (`./gradlew`, or `sbt` on PATH). +export async function runCoanaManifestFacts({ + bin, + buildOpts, + buildOptsFlag, + configs, + cwd, + ecosystem, + ignoreUnresolved, + verbose, +}: { + bin: string + buildOpts: string[] + buildOptsFlag: '--gradle-opts' | '--sbt-opts' + configs: string + cwd: string + ecosystem: 'gradle' | 'sbt' + ignoreUnresolved: boolean + verbose: boolean +}): Promise { + // `coana manifest ` emits `.socket.facts.json` by default; + // there is no `--facts` flag (the artifact-paths sidecar is reachability- + // internal and not requested here). + const coanaArgs: string[] = ['manifest', ecosystem, cwd] + if (bin) { + coanaArgs.push('--bin', bin) + } + if (configs) { + coanaArgs.push('--configs', configs) + } + if (ignoreUnresolved) { + coanaArgs.push('--ignore-unresolved') + } + if (verbose) { + coanaArgs.push('--debug') + } + // `--gradle-opts` / `--sbt-opts` are variadic on the Coana side; keep them + // last so the pass-through values don't swallow any following flags. + if (buildOpts.length) { + coanaArgs.push(buildOptsFlag, ...buildOpts) + } + + logger.log( + `Generating Socket facts for the ${ecosystem} project at \`${cwd}\` ...`, + ) + if (verbose) { + logger.log('[VERBOSE] coana args:', coanaArgs) + } + + // Stream Coana's output so the user sees build-tool progress and Coana's own + // "Socket facts file written to: ..." line. + const result = await spawnCoanaDlx( + coanaArgs, + undefined, + { cwd }, + { stdio: 'inherit' }, + ) + if (!result.ok) { + process.exitCode = 1 + logger.fail(result.message || 'Coana failed to generate Socket facts') + return + } + logger.success('Generated Socket facts') + logger.log('') + logger.log( + 'Next step is to generate a Scan by running the `socket scan create` command on the same directory.', + ) +} diff --git a/src/commands/manifest/convert-gradle-to-facts.mts b/src/commands/manifest/convert-gradle-to-facts.mts index 8e4d3a37e..a152521d9 100644 --- a/src/commands/manifest/convert-gradle-to-facts.mts +++ b/src/commands/manifest/convert-gradle-to-facts.mts @@ -1,11 +1,10 @@ -import fs from 'node:fs' -import path from 'node:path' - -import { logger } from '@socketsecurity/registry/lib/logger' -import { spawn } from '@socketsecurity/registry/lib/spawn' - -import constants from '../../constants.mts' +import { runCoanaManifestFacts } from './coana-manifest-facts.mts' +// Generates a `.socket.facts.json` for a Gradle project by delegating to the +// Coana CLI's `manifest gradle` command (which owns the Gradle init script that +// resolves the dependency graph). socket-cli no longer runs gradle itself; an +// explicit `bin` is forwarded as `--bin`, otherwise Coana defaults to +// `./gradlew`. export async function convertGradleToFacts({ bin, configs, @@ -21,167 +20,14 @@ export async function convertGradleToFacts({ ignoreUnresolved: boolean verbose: boolean }): Promise { - const rBin = path.resolve(cwd, bin) - const binExists = fs.existsSync(rBin) - const cwdExists = fs.existsSync(cwd) - - logger.group('gradle2facts:') - logger.info(`- executing: \`${rBin}\``) - if (!binExists) { - logger.warn( - `Warning: It appears the executable could not be found. An error might be printed later because of that.`, - ) - } - logger.info(`- src dir: \`${cwd}\``) - if (!cwdExists) { - logger.warn( - `Warning: It appears the src dir could not be found. An error might be printed later because of that.`, - ) - } - logger.groupEnd() - - try { - // The init script is bundled alongside the existing pom-generating one. - // See .config/rollup.dist.config.mjs:copySocketFactsInitGradle. - const initLocation = path.join( - 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, - 'socketFacts', - ] - if (verbose) { - logger.log('[VERBOSE] Executing:', [bin], ', args:', commandArgs) - } - logger.log(`Generating Socket facts from \`${bin}\` on \`${cwd}\` ...`) - const output = await execGradle(rBin, commandArgs, cwd, verbose) - if (output.code) { - process.exitCode = 1 - logger.fail(`Gradle exited with exit code ${output.code}`) - if (!verbose) { - logger.group('stderr:') - logger.error(output.stderr) - logger.groupEnd() - } - return - } - logger.success('Executed gradle successfully') - if (verbose) { - // Output already streamed; the "Reported exports:" summary lines were - // visible inline. No need to repeat them from a captured stdout. - logger.log('') - logger.log( - 'Next step is to generate a Scan by running the `socket scan create` command on the same directory.', - ) - return - } - const exports = Array.from( - output.stdout.matchAll(/^Socket facts file written to: (.*)/gm), - m => m[1], - ) - if (exports.length) { - logger.log('Reported exports:') - for (const fn of exports) { - logger.log('- ', fn) - } - } else { - // Gradle script may have skipped emission when no resolvable - // dependencies were found (see the `components.isEmpty()` branch in - // socket-facts.init.gradle). Surface the skip reason if present so - // the user understands why nothing was written. - const skipMatch = output.stdout.match( - /^\[socket-facts\] no resolvable dependencies.*/m, - ) - if (skipMatch) { - logger.warn(skipMatch[0]) - } - } - logger.log('') - logger.log( - 'Next step is to generate a Scan by running the `socket scan create` command on the same directory.', - ) - } catch (e) { - process.exitCode = 1 - logger.fail( - 'There was an unexpected error while generating Socket facts' + - (verbose ? '' : ' (use --verbose for details)'), - ) - if (verbose) { - logger.group('[VERBOSE] error:') - logger.log(e) - logger.groupEnd() - } - } -} - -async function execGradle( - bin: string, - commandArgs: string[], - cwd: string, - verbose: boolean, -): Promise<{ code: number; stdout: string; stderr: string }> { - // When verbose, stream gradle stdout/stderr directly to the user's - // terminal — no spinner, no capture. The trade-off is that the post-run - // "Reported exports:" summary is skipped (the lines were already visible - // inline). For huge builds where the user wants to see progress, this is - // the right default. Non-verbose runs still get the spinner + summary. - if (verbose) { - logger.info( - '(Running gradle with output streaming. This can take a while.)', - ) - const output = await spawn(bin, commandArgs, { cwd, stdio: 'inherit' }) - return { code: output.code, stdout: '', stderr: '' } - } - - const { spinner } = constants - let pass = false - try { - logger.info( - '(Running gradle can take a while, depending on the size of the project)', - ) - logger.info( - '(No live output. Pass --verbose to stream gradle output instead.)', - ) - spinner.start(`Running gradlew...`) - const output = await spawn(bin, commandArgs, { cwd }) - pass = true - const { code, stderr, stdout } = output - return { code, stdout, stderr } - } finally { - if (pass) { - spinner.successAndStop('Gracefully completed gradlew execution.') - } else { - spinner.failAndStop('There was an error while trying to run gradlew.') - } - } + await runCoanaManifestFacts({ + bin, + buildOpts: gradleOpts, + buildOptsFlag: '--gradle-opts', + configs, + cwd, + ecosystem: 'gradle', + ignoreUnresolved, + verbose, + }) } diff --git a/src/commands/manifest/convert-sbt-to-facts.mts b/src/commands/manifest/convert-sbt-to-facts.mts index d8dabe01b..2b4f9747e 100644 --- a/src/commands/manifest/convert-sbt-to-facts.mts +++ b/src/commands/manifest/convert-sbt-to-facts.mts @@ -1,50 +1,12 @@ -import { existsSync, promises as fs } from 'node:fs' -import os from 'node:os' -import path from 'node:path' - -import { logger } from '@socketsecurity/registry/lib/logger' -import { spawn } from '@socketsecurity/registry/lib/spawn' - -import constants from '../../constants.mts' - -// Shown when the sbt launcher dies on a modern JDK. sbt 0.13 (and some early -// 1.x) install a SecurityManager, which JDK 18+ removed, so the launcher -// throws before our plugin runs. We don't pick a JDK for the user — they own -// their toolchain — but we point them at the fix. -const JDK_HINT = - 'Hint: old sbt (0.13.x and early 1.x) cannot run on modern JDKs because the Java Security Manager was removed in JDK 18+. Run with a compatible JDK by setting JAVA_HOME (e.g. Java 11) or passing `--sbt-opts "--java-home "`.' - -// The socket-owned global base sbt compiles our plugin into. Living under the -// app data dir (not the user's `~/.sbt`) means we never mutate their sbt -// config, while persisting the compiled plugin between runs. sbt namespaces -// the compiled output by Scala/sbt version (`target/scala-2.10/sbt-0.13`, -// `target/scala-2.12/sbt-1.0`, ...), so a single base safely serves every sbt -// version with no version detection needed. -function resolveGlobalBase(): string { - const { socketAppDataPath } = constants - return socketAppDataPath - ? path.join(path.dirname(socketAppDataPath), 'sbt-facts') - : path.join(os.tmpdir(), 'socket-sbt-facts') -} - -// Drop the shipped plugin source into `/plugins/`, rewriting only -// when its content changed so sbt's incremental compiler can reuse the cache. -async function ensurePluginSource( - pluginSrcPath: string, - pluginsDir: string, -): Promise { - const source = await fs.readFile(pluginSrcPath, 'utf8') - const destPath = path.join(pluginsDir, 'SocketFactsPlugin.scala') - let current: string | undefined - if (existsSync(destPath)) { - current = await fs.readFile(destPath, 'utf8') - } - if (current !== source) { - await fs.mkdir(pluginsDir, { recursive: true }) - await fs.writeFile(destPath, source, 'utf8') - } -} - +import { runCoanaManifestFacts } from './coana-manifest-facts.mts' + +// Generates a `.socket.facts.json` for an sbt project by delegating to the +// Coana CLI's `manifest sbt` command (which owns the sbt plugin that resolves +// the dependency graph). socket-cli no longer runs sbt itself; an explicit +// `bin` is forwarded as `--bin`, otherwise Coana defaults to `sbt` on PATH. +// JDK-compatibility guidance (sbt 0.13/early 1.x cannot run on modern JDKs) is +// handled by Coana; pass a compatible JDK via `--sbt-opts "--java-home "` +// or `JAVA_HOME`. export async function convertSbtToFacts({ bin, configs, @@ -60,171 +22,14 @@ export async function convertSbtToFacts({ sbtOpts: string[] verbose: boolean }): Promise { - logger.group('sbt2facts:') - logger.info(`- executing: \`${bin}\``) - logger.info(`- src dir: \`${cwd}\``) - if (!existsSync(cwd)) { - logger.warn( - 'Warning: It appears the src dir could not be found. An error might be printed later because of that.', - ) - } - logger.groupEnd() - - try { - const pluginSrcPath = path.join( - constants.distPath, - 'socket-facts.plugin.scala', - ) - const globalBase = resolveGlobalBase() - await ensurePluginSource(pluginSrcPath, path.join(globalBase, 'plugins')) - - // `-Dsbt.global.base` points sbt at our isolated plugins dir, so the - // source-only plugin activates without touching the user's `~/.sbt`. The - // resolution options are passed as JVM system properties the plugin reads. - const socketProps: string[] = [] - if (ignoreUnresolved) { - socketProps.push('-Dsocket.ignoreUnresolved=true') - } - if (configs) { - socketProps.push(`-Dsocket.configs=${configs}`) - } - const commandArgs = [ - `-Dsbt.global.base=${globalBase}`, - ...socketProps, - ...sbtOpts, - '--batch', - 'socketFacts', - ] - if (verbose) { - logger.log('[VERBOSE] Executing:', [bin], ', args:', commandArgs) - } - logger.log(`Generating Socket facts from \`${bin}\` on \`${cwd}\` ...`) - - const output = await execSbt(bin, commandArgs, cwd, verbose) - if (output.code) { - process.exitCode = 1 - logger.fail(`sbt exited with exit code ${output.code}`) - if (!verbose) { - const errorLines = extractErrorLines(output.stdout, output.stderr) - if (errorLines) { - logger.group('sbt output:') - logger.error(errorLines) - logger.groupEnd() - } - } - if (/security ?manager/i.test(output.stdout + output.stderr)) { - logger.warn(JDK_HINT) - } - return - } - logger.success('Executed sbt successfully') - if (verbose) { - // Output already streamed inline; nothing to re-summarize. - logger.log('') - logger.log( - 'Next step is to generate a Scan by running the `socket scan create` command on the same directory.', - ) - return - } - // `spawn` already strips ANSI from captured output, and the plugin prints - // these lines bare (via println, no sbt `[info]` prefix), so plain line - // matching is stable. - const exports: string[] = [] - for (const m of output.stdout.matchAll( - /Socket facts file written to: (.+)/g, - )) { - const reported = m[1]?.trim() - if (reported) { - exports.push(reported) - } - } - if (exports.length) { - logger.log('Reported exports:') - for (const fn of exports) { - logger.log('- ', fn) - } - } else { - // The plugin skips emission when the build has no resolvable deps. - const skipMatch = output.stdout.match( - /\[socket-facts\] no resolvable dependencies.*/, - ) - if (skipMatch) { - logger.warn(skipMatch[0]) - } - } - logger.log('') - logger.log( - 'Next step is to generate a Scan by running the `socket scan create` command on the same directory.', - ) - } catch (e) { - process.exitCode = 1 - // A missing sbt launcher is the most common setup failure; surface it - // clearly instead of the generic message. - if (e instanceof Error && (e as NodeJS.ErrnoException).code === 'ENOENT') { - logger.fail( - `Could not run \`${bin}\`. Make sure sbt is installed and on your PATH, or pass --bin with the path to your sbt launcher.`, - ) - } else { - logger.fail( - 'There was an unexpected error while generating Socket facts' + - (verbose ? '' : ' (use --verbose for details)'), - ) - } - if (verbose) { - logger.group('[VERBOSE] error:') - logger.log(e) - logger.groupEnd() - } - } -} - -// Pull the actionable lines out of a noisy sbt run so a failure surfaces the -// plugin's own message (and sbt's `[error]` lines) without dumping the whole -// resolution log. -function extractErrorLines(stdout: string, stderr: string): string { - return `${stdout}\n${stderr}` - .split('\n') - .filter(line => - /\[error]|Socket facts|could not resolve|unresolved/i.test(line), - ) - .join('\n') - .trim() -} - -async function execSbt( - bin: string, - commandArgs: string[], - cwd: string, - verbose: boolean, -): Promise<{ code: number; stdout: string; stderr: string }> { - // When verbose, stream sbt output straight to the terminal so the user can - // watch resolution progress; otherwise show a spinner and capture output for - // the post-run summary. - if (verbose) { - logger.info('(Running sbt with output streaming. This can take a while.)') - const output = await spawn(bin, commandArgs, { cwd, stdio: 'inherit' }) - return { code: output.code, stdout: '', stderr: '' } - } - - const { spinner } = constants - let pass = false - try { - logger.info( - '(Running sbt can take a while, depending on the size of the project)', - ) - logger.info( - '(No live output. Pass --verbose to stream sbt output instead.)', - ) - spinner.start('Running sbt...') - const output = await spawn(bin, commandArgs, { cwd }) - pass = true - const { code, stderr, stdout } = output - return { code, stdout, stderr } - } finally { - if (pass) { - spinner.successAndStop('Gracefully completed sbt execution.') - } else { - spinner.failAndStop('There was an error while trying to run sbt.') - } - } + await runCoanaManifestFacts({ + bin, + buildOpts: sbtOpts, + buildOptsFlag: '--sbt-opts', + configs, + cwd, + ecosystem: 'sbt', + ignoreUnresolved, + verbose, + }) } diff --git a/src/commands/manifest/generate_auto_manifest.mts b/src/commands/manifest/generate_auto_manifest.mts index 43b89d62e..22a8ee299 100644 --- a/src/commands/manifest/generate_auto_manifest.mts +++ b/src/commands/manifest/generate_auto_manifest.mts @@ -51,7 +51,9 @@ export async function generateAutoManifest({ .filter(Boolean) ?? [], verbose: Boolean(sockJson.defaults?.manifest?.sbt?.verbose), } - if (sockJson.defaults?.manifest?.sbt?.facts) { + // Socket facts is the default; opt into pom generation with + // `defaults.manifest.sbt.facts: false` in socket.json. + if (sockJson.defaults?.manifest?.sbt?.facts !== false) { logger.log('Detected a Scala sbt build, generating Socket facts...') await convertSbtToFacts({ ...sbtArgs, @@ -85,7 +87,9 @@ export async function generateAutoManifest({ .map(s => s.trim()) .filter(Boolean) ?? [], } - if (sockJson.defaults?.manifest?.gradle?.facts) { + // Socket facts is the default; opt into pom generation with + // `defaults.manifest.gradle.facts: false` in socket.json. + if (sockJson.defaults?.manifest?.gradle?.facts !== false) { logger.log( 'Detected a gradle build (Gradle, Kotlin, Scala), generating Socket facts...', ) diff --git a/src/commands/manifest/generate_auto_manifest.test.mts b/src/commands/manifest/generate_auto_manifest.test.mts index 07c22f03b..c454d2998 100644 --- a/src/commands/manifest/generate_auto_manifest.test.mts +++ b/src/commands/manifest/generate_auto_manifest.test.mts @@ -14,6 +14,12 @@ vi.mock('./convert_gradle_to_maven.mts', () => ({ vi.mock('./convert_sbt_to_maven.mts', () => ({ convertSbtToMaven: vi.fn(async () => undefined), })) +vi.mock('./convert-gradle-to-facts.mts', () => ({ + convertGradleToFacts: vi.fn(async () => undefined), +})) +vi.mock('./convert-sbt-to-facts.mts', () => ({ + convertSbtToFacts: vi.fn(async () => undefined), +})) vi.mock('./handle-manifest-conda.mts', () => ({ handleManifestConda: vi.fn(async () => undefined), })) @@ -22,6 +28,7 @@ vi.mock('../../utils/socket-json.mts', () => ({ })) import { extractBazelToMaven } from './bazel/extract_bazel_to_maven.mts' +import { convertGradleToFacts } from './convert-gradle-to-facts.mts' import { convertGradleToMaven } from './convert_gradle_to_maven.mts' import { generateAutoManifest } from './generate_auto_manifest.mts' import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' @@ -40,6 +47,7 @@ const baseDetected = { describe('generateAutoManifest — bazel branch', () => { beforeEach(() => { vi.mocked(extractBazelToMaven).mockClear() + vi.mocked(convertGradleToFacts).mockClear() vi.mocked(convertGradleToMaven).mockClear() vi.mocked(readOrDefaultSocketJson).mockReturnValue({} as SocketJson) vi.mocked(extractBazelToMaven).mockResolvedValue({ @@ -202,7 +210,23 @@ describe('generateAutoManifest — bazel branch', () => { verbose: false, }) expect(extractBazelToMaven).toHaveBeenCalledTimes(1) + // Socket facts is the default for the gradle branch. + expect(convertGradleToFacts).toHaveBeenCalledTimes(1) + expect(convertGradleToMaven).not.toHaveBeenCalled() + }) + + it('uses the gradle pom generator when defaults.manifest.gradle.facts is false', async () => { + vi.mocked(readOrDefaultSocketJson).mockReturnValue({ + defaults: { manifest: { gradle: { facts: false } } }, + } as SocketJson) + await generateAutoManifest({ + cwd: '/tmp/repo', + detected: { ...baseDetected, gradle: true, count: 1 }, + outputKind: 'text', + verbose: false, + }) expect(convertGradleToMaven).toHaveBeenCalledTimes(1) + expect(convertGradleToFacts).not.toHaveBeenCalled() }) it('honors socket.json out override (user-supplied .socket-auto-manifest dir)', async () => { diff --git a/src/commands/manifest/socket-facts.init.gradle b/src/commands/manifest/socket-facts.init.gradle deleted file mode 100644 index b9af15933..000000000 --- a/src/commands/manifest/socket-facts.init.gradle +++ /dev/null @@ -1,429 +0,0 @@ -// Gradle init script that emits a single `.socket.facts.json` file at the -// build root describing the resolved compile/runtime dependency graph of -// every subproject combined. -// -// Schema matches the canonical SocketFacts shape consumed by depscan -// (`workspaces/lib/src/socket-facts/socket-facts-schema.ts`): -// -// { components: SF_Artifact[] } -// -// Each Maven SF_Artifact is `{ type: 'maven', namespace, name, version?, -// qualifiers? } & { id, direct?, dev?, tooling?, dependencies? }`. -// `qualifiers` is strict on `{ classifier?, ext? }` — anything else is -// dropped. -// -// Invoke via: -// ./gradlew --init-script socket-facts.init.gradle socketFacts -// -// Structure: -// - per-subproject `socketFactsCollect` tasks resolve that subproject's -// configurations and contribute to shared accumulators on gradle.ext -// - the root `socketFacts` task depends on every collector, then -// serializes the accumulated graph to a single JSON file at the build -// root -// -// Intra-project dependencies (i.e. `project(':lib')` style edges between -// subprojects in the same build) are dropped from the output entirely. -// Their reasoning: each subproject contributes its own external deps to -// the shared facts; the inter-project edges would just be noise that -// downstream consumers (coana mvn dependency:get) would try to resolve -// against Maven Central and fail. The externals each intra-project dep -// brings in are picked up via that subproject's own collector. - -import java.util.Collections -import groovy.json.JsonOutput - -// Must stay in sync with `DOT_SOCKET_DOT_FACTS_JSON` in -// src/constants.mts (TS side). Groovy can't import the TS constant, so -// the two strings are intentionally duplicated; if you change one, -// change the other. -ext.SOCKET_FACTS_FILENAME = '.socket.facts.json' - -// Shared accumulators across all subprojects' contributions. Synchronized -// collections so --parallel-enabled builds don't race. The accumulator -// lives on `gradle.ext` so every subproject's collector and the root -// aggregator share the same instance. -gradle.ext.socketFactsState = [ - // id -> [coord, children, prod, nonTooling] - nodes : Collections.synchronizedMap([:]), - // first-level dep ids - directIds : Collections.synchronizedSet([] as Set), - // selectors we've already logged as unresolved (deduped across configs) - reportedUnresolved : Collections.synchronizedSet([] as Set), - // "group:name" of every project in this build — used to filter - // intra-project deps. Populated once all projects are evaluated. - projectKeys : Collections.synchronizedSet([] as Set), -] - -// Capture every project's (group:name) once all projects are configured so -// per-subproject collectors can filter intra-project deps without an -// ordering dependency on other subprojects. -gradle.projectsEvaluated { g -> - g.rootProject.allprojects.each { p -> - g.socketFactsState.projectKeys.add("${p.group ?: ''}:${p.name}") - } -} - -allprojects { project -> - def collectTask = project.tasks.create('socketFactsCollect') { - description = "Resolves ${project.path}'s configurations into the build-wide Socket facts accumulator" - // Dependency resolution depends on state Gradle's up-to-date tracking - // can't represent reliably. - outputs.upToDateWhen { false } - - doLast { - def state = gradle.socketFactsState - def nodes = state.nodes - def directIds = state.directIds - def reportedUnresolved = state.reportedUnresolved - def projectKeys = state.projectKeys - - // `id` omits ext so Gradle's variant artifacts (e.g. - // `java-classes-directory` and `jar` for the same project dep) - // dedupe into a single component. Classifier stays in the id since - // it identifies a distinct artifact (sources, javadoc, etc.). - def coordId = { coord -> - def parts = [coord.groupId, coord.artifactId] - if (coord.classifier) parts << coord.classifier - parts << coord.version - parts.join(':') - } - - def isIntraProject = { String group, String name -> - projectKeys.contains("${group ?: ''}:${name}") - } - - // Atomic upsert: bracket the read-modify-write under the nodes map's - // monitor so concurrent contributions don't lose flag updates. - def upsertNode = { Map coord, boolean isProd, boolean isNonTooling -> - def id = coordId(coord) - synchronized (nodes) { - def node = nodes[id] - if (node == null) { - node = [coord: coord, children: [] as Set, prod: false, nonTooling: false] - nodes[id] = node - } else if (!node.coord.ext && coord.ext) { - // Upgrade to the variant whose Gradle artifact has a real - // packaging extension. Compile classpath visits often arrive - // with no ext (a project dep exposes only its classes-directory - // variant there); the runtime classpath visit then fills in - // the canonical jar/aar. - node.coord = coord - } - if (isProd) { - node.prod = true - } - if (isNonTooling) { - node.nonTooling = true - } - } - id - } - - // Walk a resolved dependency, emitting nodes for itself and its - // transitive closure. `cache` is keyed by ResolvedDependency identity - // and short-circuits revisits in diamond/cyclic graphs. - // - // We never touch `artifact.file` — that forces Gradle to *download* - // the underlying file (catastrophic on large builds that declare - // distribution archives as dependencies). `artifact.extension` and - // `artifact.classifier` read from metadata that resolution already - // needed. - // - // Intra-project deps (project(':lib') and friends) are dropped at - // visit time: we return an empty produced-id set, don't emit a node, - // and don't recurse into the dep's children. The transitives those - // intra-project deps expose are picked up via the consumer - // subproject's classpath directly (Gradle merges them) and via the - // intra-project's own collector. - def visit - visit = { dep, boolean isProd, boolean isNonTooling, Map cache -> - if (cache.containsKey(dep)) { - return cache[dep] - } - if (isIntraProject(dep.moduleGroup, dep.moduleName)) { - def empty = [] as Set - cache[dep] = empty - return empty - } - // Pre-populate the cache to break cycles before we recurse. - def producedIds = [] as Set - cache[dep] = producedIds - - def artifacts = dep.moduleArtifacts - if (artifacts.isEmpty()) { - producedIds << upsertNode([ - groupId : dep.moduleGroup ?: '', - artifactId: dep.moduleName, - version : dep.moduleVersion ?: '', - classifier: '', - ext : '', - ], isProd, isNonTooling) - } else { - artifacts.each { a -> - producedIds << upsertNode([ - groupId : dep.moduleGroup ?: '', - artifactId: dep.moduleName, - version : dep.moduleVersion ?: '', - classifier: a.classifier ?: '', - // Use the file extension Gradle reports. For Gradle-internal - // directory variants (java-classes-directory etc.) the - // extension is empty — we let that through and emit no ext - // qualifier. Never fall back to artifact.type, which is - // Gradle's variant attribute, not Maven packaging. - ext : a.extension ?: '', - ], isProd, isNonTooling) - } - } - - def childIds = [] as Set - dep.children.each { child -> - childIds.addAll(visit(child, isProd, isNonTooling, cache)) - } - synchronized (nodes) { - producedIds.each { pid -> - nodes[pid].children.addAll(childIds) - } - } - producedIds - } - - // Configuration selection by name pattern. We match the conventional - // suffixes used across Gradle plugins for resolvable classpath configs: - // Java (`compileClasspath`, `runtimeClasspath`, - // `testCompileClasspath`, `testRuntimeClasspath`), Kotlin Gradle Plugin - // (`jvmMainCompileClasspath`, `linuxX64MainRuntimeClasspath`, ...) and - // AGP per-variant (`debugCompileClasspath`, `releaseRuntimeClasspath`, - // `debugUnitTestRuntimeClasspath`, ...). - // - // Beyond classpaths we also walk other resolvable configurations - // (annotation processors, linter classpaths, etc.) so build-tooling - // deps land in the output too — tagged `tooling: true` so downstream - // reachability scanners can skip them. - // - // We exclude AGP's instrumented-test classpaths (`*AndroidTest*`) - // because their variant resolution requires consumer attributes - // (target SDK, device/host runtime) that an init-script-driven - // resolution doesn't set, and they produce ambiguity errors at - // resolution time. Unit-test classpaths (`*UnitTest*`) resolve fine. - def isClasspath = { String name -> - def lower = name.toLowerCase() - lower.endsWith('compileclasspath') || lower.endsWith('runtimeclasspath') - } - def isAndroidInstrumentedTest = { String name -> - name.toLowerCase().contains('androidtest') - } - 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 { - 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 -> - def isProd = !isTestConfig(cfg.name) - def isNonTooling = isClasspath(cfg.name) - // Per-configuration try/catch: AGP-style configurations can fail - // with "variant ambiguity" when resolved from an init-script - // context that doesn't carry the consumer attributes AGP sets - // internally. We log and continue so a single ambiguous config - // doesn't sink the whole facts file. - try { - def lenient = cfg.resolvedConfiguration.lenientConfiguration - def cache = [:] - 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 - } - def selectorKey = dep.selector.toString() - if (reportedUnresolved.add(selectorKey)) { - def reason = dep.problem?.message?.readLines()?.first() ?: 'unknown reason' - println "[socket-facts] unresolved: ${selectorKey} in ${project.path}: ${reason}" - } - } - } catch (Exception e) { - println "[socket-facts] skipping ${project.path}:${cfg.name}: ${e.message?.readLines()?.first()}" - } - } - } - } -} - -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 } - - doLast { - 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 - // (`aggregator.dependsOn(collector)`) already guarantee a - // happens-before edge between writes and this read, but we - // synchronize on `nodes` here so the read path is symmetric with the - // write path — no implicit reliance on Gradle's task-graph ordering - // semantics for memory visibility of plain HashMap/HashSet fields. - def components - synchronized (nodes) { - components = nodes.collect { id, node -> - [id: id, coord: node.coord, prod: node.prod, nonTooling: node.nonTooling, children: (node.children as List).sort()] - } - } - - components = components.collect { snapshot -> - def id = snapshot.id - def coord = snapshot.coord - def component = [ - type : 'maven', - namespace: coord.groupId, - name : coord.artifactId, - ] - if (coord.version) { - component.version = coord.version - } - def qualifiers = [:] - if (coord.classifier) { - qualifiers.classifier = coord.classifier - } - if (coord.ext) { - qualifiers.ext = coord.ext - } - if (!qualifiers.isEmpty()) { - component.qualifiers = qualifiers - } - component.id = id - if (directIds.contains(id)) { - component.direct = true - } - if (!snapshot.prod) { - component.dev = true - } - if (!snapshot.nonTooling) { - component.tooling = true - } - if (!snapshot.children.isEmpty()) { - component.dependencies = snapshot.children - } - component - } - - if (components.isEmpty()) { - println "[socket-facts] no resolvable dependencies in build, skipping" - return - } - - def outputDir = outDirOverride ? new File(outDirOverride) : defaultOutDir - outputDir.mkdirs() - 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}" - } - } -} - -// Wire every subproject's collector as a dependency of the root aggregator -// so the aggregator runs after all contributions have been made. -gradle.projectsEvaluated { g -> - def aggregator = g.rootProject.tasks.findByName('socketFacts') - if (aggregator) { - g.rootProject.allprojects.each { p -> - def collector = p.tasks.findByName('socketFactsCollect') - if (collector) { - aggregator.dependsOn(collector) - } - } - } -} diff --git a/src/commands/manifest/socket-facts.plugin.scala b/src/commands/manifest/socket-facts.plugin.scala deleted file mode 100644 index ba2f50b7b..000000000 --- a/src/commands/manifest/socket-facts.plugin.scala +++ /dev/null @@ -1,416 +0,0 @@ -package socket - -import sbt._ -import sbt.Keys._ - -import org.apache.ivy.Ivy -import org.apache.ivy.core.cache.DefaultRepositoryCacheManager -import org.apache.ivy.core.module.descriptor.{ Artifact, ModuleDescriptor } -import org.apache.ivy.core.module.id.ModuleRevisionId -import org.apache.ivy.core.report.ResolveReport -import org.apache.ivy.core.resolve.{ IvyNode, ResolveOptions } - -import scala.collection.mutable - -/** - * Socket facts plugin for sbt. - * - * Emits a single `.socket.facts.json` at the build root describing the - * resolved dependency graph of every project in the build, in the canonical - * SocketFacts schema (mirrors socket-facts.init.gradle on the gradle side): - * - * { "components": SF_Artifact[] } - * - * Each Maven component is - * { type: 'maven', namespace, name, version?, qualifiers? } & - * { id, direct?, dev?, dependencies? } - * - * The graph is read from Ivy resolution metadata only: `setDownload(false)` - * means no artifact jars are fetched, just the POM/ivy.xml needed to compute - * the transitive closure — we never consume bandwidth pulling jars. - * - * By default only the project's real dependency configurations are resolved - * (`compile`, `runtime`, `test`, `provided`, `optional`); the Scala - * compiler/scaladoc toolchain, sbt-native-packager configs (debian, docker, - * 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` 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 - * configs are tagged `dev`. - * - * Unresolved dependencies are fatal by default (non-zero exit) rather than - * silently dropped — the env is the user's own, set up to resolve their deps. - * `-Dsocket.ignoreUnresolved=true` downgrades that to a warning and emits the - * resolvable deps anyway. - * - * Intra-build project dependencies are omitted: sbt's `dependsOn` is a - * classpath dependency, not an Ivy one, so siblings rarely appear in a - * project's resolve, and a sibling referenced as an explicit library - * dependency is filtered out by coordinate. Each project's own external deps - * are aggregated, and resolution is per-project, so divergent per-module - * versions are all reported. - * - * Delivery: shipped as source and dropped into an isolated `-Dsbt.global.base` - * plugins dir, so it activates on any project without installation. It is - * compiled by the sbt meta-build, whose Scala is 2.10 for sbt 0.13 and 2.12 - * for sbt 1.x — so this file must compile on both. Reaching into Ivy (stable - * across those sbt versions) keeps the code free of the version-specific sbt - * APIs that would otherwise need reflection, and lets us scope resolution to - * the configs we want (which sbt's own `update`/`updateFull` can't). - */ -object SocketFactsPlugin extends AutoPlugin { - override def trigger = allRequirements - - // The configurations resolved by default: the project's real dependency - // scopes. Override via `-Dsocket.configs`. Everything else (toolchain, - // packager, `-internal`, sources/docs/pom) is skipped — not declared deps, - // and costly to resolve. - private val DefaultConfs = - Set("compile", "optional", "provided", "runtime", "test") - - // Must stay in sync with `DOT_SOCKET_DOT_FACTS_JSON` in src/constants.mts - // (TS side). Scala can't import the TS constant, so the two strings are - // intentionally duplicated; change them together. - private val SocketFactsFilename = ".socket.facts.json" - - object autoImport { - val socketFacts = - taskKey[Unit]("Emit a Socket facts JSON for the whole build") - } - import autoImport._ - - override def projectSettings: Seq[Setting[_]] = Seq( - // Run once for the whole build; the task itself gathers every project via - // ScopeFilter, so we don't want sbt to also fan it out to aggregates. - // Note: `in` (not the newer `key / scope` slash syntax) is intentional — - // slash syntax doesn't exist in sbt 0.13, which we still support. The - // 1.5+ deprecation warning it triggers is harmless and only surfaces on a - // cold compile. Same goes for `baseDirectory in ThisBuild` below. - aggregate in socketFacts := false, - socketFacts := { - val log = streams.value.log - val modules = ivyModule.all(ScopeFilter(inAnyProject)).value - val buildRoot = (baseDirectory in ThisBuild).value - - // First pass: every project's own coordinate (org:name:version), so - // intra-build deps are omitted even when referenced as explicit library - // deps. Keying on the full GAV (not just org:name) means an external dep - // that merely shares an org:name with a build project is still emitted. - val projectCoords = mutable.HashSet.empty[String] - modules.foreach { module => - module.withModule(log) { (_, md, _) => - projectCoords += gavKey(md.getModuleRevisionId) - } - } - - val nodes = mutable.LinkedHashMap.empty[String, Node] - val unresolved = mutable.LinkedHashSet.empty[String] - - // Second pass: resolve each project metadata-only and fold its graph in. - modules.foreach { module => - module.withModule(log) { (ivy, md, _) => - collectResolved(ivy, md, projectCoords, nodes, unresolved) - } - } - - if (unresolved.nonEmpty) { - val ignore = boolProp("socket.ignoreUnresolved") - if (ignore) { - log.warn( - "Socket facts: skipping " + unresolved.size + - " unresolved dependency(ies) (ignore-unresolved):" - ) - unresolved.toList.sorted.foreach(u => log.warn(" - " + u)) - } else { - log.error("Socket facts: could not resolve these dependencies:") - unresolved.toList.sorted.foreach(u => log.error(" - " + u)) - sys.error( - "Socket facts aborted: " + unresolved.size + - " unresolved dependency(ies). Pass --ignore-unresolved to skip " + - "them, or fix resolution (repositories, credentials, offline " + - "cache) and retry." - ) - } - } - - if (nodes.isEmpty) { - // println (not log.info) so the line reaches stdout without sbt's - // `[info]` prefix, matching what the CLI parses for. - println("[socket-facts] no resolvable dependencies in build, skipping") - } else { - val outDir = sys.props.get("socket.outputDirectory") match { - case Some(d) if d.nonEmpty => new File(d) - case _ => buildRoot - } - outDir.mkdirs() - val outName = sys.props.get("socket.outputFile") match { - case Some(f) if f.nonEmpty => f - case _ => SocketFactsFilename - } - val outFile = new File(outDir, outName) - IO.write(outFile, renderJson(nodes)) - // println (not log.info) so the line reaches stdout without sbt's - // `[info]` prefix, matching what the CLI parses for. - println("Socket facts file written to: " + outFile.getAbsolutePath) - } - } - ) - - // 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 => - 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")) - - // Resolve one project's module metadata-only and fold its graph into the - // shared, build-wide node map. Takes the stable Ivy types (not sbt's - // IvySbt#Module, which moved packages between 0.13 and 1.x). - private def collectResolved( - ivy: Ivy, - md: ModuleDescriptor, - projectCoords: scala.collection.Set[String], - nodes: mutable.LinkedHashMap[String, Node], - unresolved: mutable.LinkedHashSet[String] - ): Unit = { - val rootMrid = md.getModuleRevisionId - 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 - // cached module per resolve is pure overhead (~30% of warm-cache time). - // Missing metadata is still fetched (this is not cache-only), so cold - // caches still work — we just never re-check what we already have. - ivy.getSettings.getDefaultRepositoryCacheManager match { - case drcm: DefaultRepositoryCacheManager => - drcm.setCheckmodified(false) - drcm.setUseOrigin(true) - drcm.setDefaultTTL(Long.MaxValue) - case _ => - } - val options = new ResolveOptions() - options.setDownload(false) - options.setTransitive(true) - options.setConfs(confs) - // Skip Ivy's report rendering — it re-walks the graph and we don't use it. - options.setOutputReport(false) - val report: ResolveReport = ivy.resolve(md, options) - - report.getUnresolvedDependencies.foreach { node => - unresolved += node.getId.toString - } - - // Pass 1: emit one node per resolved module, and remember which - // component id each Ivy module maps to (for wiring caller edges). - val mridToId = mutable.HashMap.empty[String, String] - val pass1 = report.getDependencies.iterator() - while (pass1.hasNext) { - val ivyNode = pass1.next().asInstanceOf[IvyNode] - if (isEmittable(ivyNode, projectCoords)) { - val mrid = ivyNode.getResolvedId - // prod = reached by any non-test config; a dep reached only via test - // configs is dev. - val prod = - ivyNode.getRootModuleConfigurations.exists(c => !isTestConf(c)) - val coord = coordFor(ivyNode, mrid) - val node = nodes.getOrElseUpdate(coord.id, new Node(coord)) - if (prod) { - node.prod = true - } - mridToId(mrid.toString) = coord.id - } - } - - // Pass 2: wire caller edges. A caller that is the project root marks the - // node `direct`; any other caller becomes its parent. - val pass2 = report.getDependencies.iterator() - while (pass2.hasNext) { - val ivyNode = pass2.next().asInstanceOf[IvyNode] - if (isEmittable(ivyNode, projectCoords)) { - mridToId.get(ivyNode.getResolvedId.toString).foreach { childId => - ivyNode.getAllCallers.foreach { caller => - val callerMrid = caller.getModuleRevisionId - if (callerMrid == rootMrid) { - nodes(childId).direct = true - } else { - mridToId - .get(callerMrid.toString) - .foreach(parentId => nodes(parentId).children += childId) - } - } - } - } - } - } - } - - // A configuration whose name mentions "test" (test, test-internal, - // IntegrationTest, ...) contributes dev dependencies. Name-based, mirroring - // the gradle script, so it also catches custom test-like configs. - private def isTestConf(name: String): Boolean = - name.toLowerCase.contains("test") - - // A dependency node is emittable when it actually resolved to a real module - // that isn't a project in this build and isn't a conflict loser. Failed or - // unloaded nodes are skipped here (reading their metadata throws); they're - // reported separately via the resolve report's unresolved list. - private def isEmittable( - node: IvyNode, - projectCoords: scala.collection.Set[String] - ): Boolean = { - val mrid = node.getResolvedId - mrid != null && - node.getModuleRevision != null && - !node.hasProblem && - !projectCoords.contains(gavKey(mrid)) && - !node.isCompletelyEvicted - } - - // Build-wide coordinate key (org:name:version) identifying a project in this - // build, so its own modules are omitted when they surface in another - // project's resolve. Full GAV (not just org:name) so a same-named external - // dependency at a different version is still emitted. - private def gavKey(mrid: ModuleRevisionId): String = - mrid.getOrganisation + ":" + mrid.getName + ":" + mrid.getRevision - - private def coordFor(node: IvyNode, mrid: ModuleRevisionId): Coord = - Coord(mrid.getOrganisation, mrid.getName, mrid.getRevision, primaryExt(node)) - - // The packaging extension of the module's main (classifier-less) artifact. - // Reading artifact metadata never triggers a download. Defaults to jar, - // which is correct for the overwhelming majority of Maven dependencies. - private def primaryExt(node: IvyNode): String = { - val artifacts = node.getAllArtifacts - if (artifacts == null) { - "jar" - } else { - artifacts.find(a => classifierOf(a).isEmpty).map(extOf).getOrElse("jar") - } - } - - private def extOf(a: Artifact): String = { - val e = a.getExt - if (e == null || e.isEmpty) "jar" else e - } - - private def classifierOf(a: Artifact): String = { - val extra = a.getExtraAttributes - val raw = - if (extra.get("classifier") != null) extra.get("classifier") - else extra.get("m:classifier") - if (raw == null) "" else raw.toString - } - - private def renderJson(nodes: mutable.LinkedHashMap[String, Node]): String = { - val sorted = nodes.values.toList.sortBy(_.coord.id) - val sb = new StringBuilder - sb.append("{\n \"components\": [\n") - sorted.zipWithIndex.foreach { - case (node, idx) => - appendComponent(sb, node) - if (idx < sorted.size - 1) { - sb.append(",") - } - sb.append("\n") - } - sb.append(" ]\n}\n") - sb.toString - } - - private def appendComponent(sb: StringBuilder, node: Node): Unit = { - val c = node.coord - val fields = mutable.ListBuffer.empty[String] - fields += "\"type\": \"maven\"" - fields += "\"namespace\": " + jsonString(c.org) - fields += "\"name\": " + jsonString(c.name) - if (c.version.nonEmpty) { - fields += "\"version\": " + jsonString(c.version) - } - if (c.ext.nonEmpty) { - fields += "\"qualifiers\": { \"ext\": " + jsonString(c.ext) + " }" - } - fields += "\"id\": " + jsonString(c.id) - if (node.direct) { - fields += "\"direct\": true" - } - if (!node.prod) { - fields += "\"dev\": true" - } - if (node.children.nonEmpty) { - val depLines = node.children.toList.map(d => " " + jsonString(d)) - fields += "\"dependencies\": [\n" + depLines.mkString(",\n") + "\n ]" - } - sb.append(" {\n ") - sb.append(fields.mkString(",\n ")) - sb.append("\n }") - } - - private def jsonString(s: String): String = { - val sb = new StringBuilder("\"") - s.foreach { - case '"' => sb.append("\\\"") - case '\\' => sb.append("\\\\") - case '\n' => sb.append("\\n") - case '\r' => sb.append("\\r") - case '\t' => sb.append("\\t") - case ch if ch < 0x20 => sb.append("\\u%04x".format(ch.toInt)) - case ch => sb.append(ch) - } - sb.append("\"") - sb.toString - } - - // A resolved Maven coordinate. - private final case class Coord( - org: String, - name: String, - version: String, - ext: String - ) { - val id: String = org + ":" + name + ":" + version - } - - private final class Node(val coord: Coord) { - val children = mutable.TreeSet.empty[String] - var prod = false - var direct = false - } -} diff --git a/src/constants.mts b/src/constants.mts index daefb4fcc..553de604c 100644 --- a/src/constants.mts +++ b/src/constants.mts @@ -199,10 +199,8 @@ const CONFIG_KEY_API_TOKEN = 'apiToken' const CONFIG_KEY_DEFAULT_ORG = 'defaultOrg' const CONFIG_KEY_ENFORCED_ORGS = 'enforcedOrgs' const CONFIG_KEY_ORG = 'org' -// Must stay in sync with `ext.SOCKET_FACTS_FILENAME` in -// src/commands/manifest/socket-facts.init.gradle (Groovy side). -// Groovy can't import a TS constant, so the two values are intentionally -// duplicated; change them together. +// The Socket facts filename. The file itself is generated by the Coana CLI's +// `manifest` commands; this constant must match the filename Coana emits. const DOT_SOCKET_DOT_FACTS_JSON = `${DOT_SOCKET_DIR}.facts.json` const DLX_BINARY_CACHE_TTL = 7 * 24 * 60 * 60 * 1_000 // 7 days in milliseconds. const DRY_RUN_LABEL = '[DryRun]' diff --git a/test/fixtures/commands/manifest/gradle-facts/.gitignore b/test/fixtures/commands/manifest/gradle-facts/.gitignore deleted file mode 100644 index 5bf330e95..000000000 --- a/test/fixtures/commands/manifest/gradle-facts/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Generated by `socket manifest gradle --facts` runs against any of the -# fixtures in this directory. -.gradle/ -build/ -.socket.facts.json diff --git a/test/fixtures/commands/manifest/gradle-facts/android-library/build.gradle b/test/fixtures/commands/manifest/gradle-facts/android-library/build.gradle deleted file mode 100644 index f555aae3d..000000000 --- a/test/fixtures/commands/manifest/gradle-facts/android-library/build.gradle +++ /dev/null @@ -1,32 +0,0 @@ -// Minimal AGP fixture for socket-facts.init.gradle. Exercises Android's -// per-variant compile/runtime classpath configurations (debugCompileClasspath, -// releaseRuntimeClasspath, debugUnitTestRuntimeClasspath, ...) that the Java -// SourceSetContainer doesn't surface. -// -// Requires: -// - JDK 17+ -// - Gradle 8.7+ (matched to the AGP version below) -// - An Android SDK reachable via ANDROID_HOME / ANDROID_SDK_ROOT or -// local.properties (gitignored). -plugins { - id 'com.android.library' version '8.7.3' -} - -android { - namespace 'com.example.socket.androidlib' - compileSdk 34 - - defaultConfig { - minSdk 24 - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } -} - -dependencies { - implementation 'androidx.annotation:annotation:1.7.1' - testImplementation 'junit:junit:4.13.2' -} diff --git a/test/fixtures/commands/manifest/gradle-facts/android-library/gradle.properties b/test/fixtures/commands/manifest/gradle-facts/android-library/gradle.properties deleted file mode 100644 index 5bac8ac50..000000000 --- a/test/fixtures/commands/manifest/gradle-facts/android-library/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -android.useAndroidX=true diff --git a/test/fixtures/commands/manifest/gradle-facts/android-library/settings.gradle b/test/fixtures/commands/manifest/gradle-facts/android-library/settings.gradle deleted file mode 100644 index 520ef3021..000000000 --- a/test/fixtures/commands/manifest/gradle-facts/android-library/settings.gradle +++ /dev/null @@ -1,16 +0,0 @@ -pluginManagement { - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -dependencyResolutionManagement { - repositories { - google() - mavenCentral() - } -} - -rootProject.name = 'android-library' diff --git a/test/fixtures/commands/manifest/gradle-facts/kotlin-multiplatform/build.gradle b/test/fixtures/commands/manifest/gradle-facts/kotlin-multiplatform/build.gradle deleted file mode 100644 index 4f87e73fa..000000000 --- a/test/fixtures/commands/manifest/gradle-facts/kotlin-multiplatform/build.gradle +++ /dev/null @@ -1,37 +0,0 @@ -// Kotlin Multiplatform fixture for socket-facts.init.gradle. Exercises -// per-target compile/runtime configurations (jvmMainCompileClasspath, -// jsTestRuntimeClasspath, etc.) that the Java SourceSetContainer doesn't -// surface, but which our name-pattern selection (`*CompileClasspath` / -// `*RuntimeClasspath`) still picks up. -// Kotlin 2.1.x is required for Gradle 9.x compatibility. KGP 1.9.x -// officially supports Gradle 6.8.3-8.6 only; CI runs Gradle 9.2.1. -plugins { - id 'org.jetbrains.kotlin.multiplatform' version '2.1.0' -} - -group = 'com.example.socket.kmp' -version = '1.0.0' - -repositories { - mavenCentral() -} - -kotlin { - jvm() - js { - nodejs() - } - - sourceSets { - commonMain { - dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.2' - } - } - jvmMain { - dependencies { - implementation 'org.slf4j:slf4j-api:1.7.36' - } - } - } -} diff --git a/test/fixtures/commands/manifest/gradle-facts/kotlin-multiplatform/settings.gradle b/test/fixtures/commands/manifest/gradle-facts/kotlin-multiplatform/settings.gradle deleted file mode 100644 index a06570ebc..000000000 --- a/test/fixtures/commands/manifest/gradle-facts/kotlin-multiplatform/settings.gradle +++ /dev/null @@ -1,9 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - mavenCentral() - google() - } -} - -rootProject.name = 'kotlin-multiplatform' diff --git a/test/fixtures/commands/manifest/gradle-facts/multi-module-java/app/build.gradle b/test/fixtures/commands/manifest/gradle-facts/multi-module-java/app/build.gradle deleted file mode 100644 index bb8dc4e27..000000000 --- a/test/fixtures/commands/manifest/gradle-facts/multi-module-java/app/build.gradle +++ /dev/null @@ -1,8 +0,0 @@ -plugins { - id 'java-library' -} - -dependencies { - implementation project(':lib') - implementation 'org.slf4j:slf4j-api:1.7.36' -} diff --git a/test/fixtures/commands/manifest/gradle-facts/multi-module-java/build.gradle b/test/fixtures/commands/manifest/gradle-facts/multi-module-java/build.gradle deleted file mode 100644 index 9ad0f8369..000000000 --- a/test/fixtures/commands/manifest/gradle-facts/multi-module-java/build.gradle +++ /dev/null @@ -1,8 +0,0 @@ -allprojects { - group = 'com.example.socket' - version = '1.0.0' - - repositories { - mavenCentral() - } -} diff --git a/test/fixtures/commands/manifest/gradle-facts/multi-module-java/lib/build.gradle b/test/fixtures/commands/manifest/gradle-facts/multi-module-java/lib/build.gradle deleted file mode 100644 index 5d40be4e2..000000000 --- a/test/fixtures/commands/manifest/gradle-facts/multi-module-java/lib/build.gradle +++ /dev/null @@ -1,8 +0,0 @@ -plugins { - id 'java-library' -} - -dependencies { - api 'com.google.guava:guava:31.1-jre' - testImplementation 'junit:junit:4.13.2' -} diff --git a/test/fixtures/commands/manifest/gradle-facts/multi-module-java/settings.gradle b/test/fixtures/commands/manifest/gradle-facts/multi-module-java/settings.gradle deleted file mode 100644 index c7597fad0..000000000 --- a/test/fixtures/commands/manifest/gradle-facts/multi-module-java/settings.gradle +++ /dev/null @@ -1,4 +0,0 @@ -rootProject.name = 'multi-module-java' - -include 'lib' -include 'app' diff --git a/test/fixtures/commands/manifest/gradle-facts/single-module-java/build.gradle b/test/fixtures/commands/manifest/gradle-facts/single-module-java/build.gradle deleted file mode 100644 index 7137608a2..000000000 --- a/test/fixtures/commands/manifest/gradle-facts/single-module-java/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -plugins { - id 'java-library' -} - -group = 'com.example.socket' -version = '1.0.0' - -repositories { - mavenCentral() -} - -// Versions chosen to resolve cleanly across Gradle 5.6.4 through 9.2.1. -// guava 32+ publishes Gradle Module Metadata that Gradle 6.9.4 can't fully -// parse (its transitives go missing); guava 31.1-jre uses POM-only metadata -// the older resolver understands. -dependencies { - api 'com.google.guava:guava:31.1-jre' - implementation 'org.slf4j:slf4j-api:1.7.36' - testImplementation 'junit:junit:4.13.2' - // Exercises the `tooling: true` flag. Lombok lives on the - // `annotationProcessor` configuration, not on any compile/runtime - // classpath — so it should be emitted with tooling=true and never have - // production deps spuriously flagged as tooling. - annotationProcessor 'org.projectlombok:lombok:1.18.30' -} diff --git a/test/fixtures/commands/manifest/gradle-facts/single-module-java/settings.gradle b/test/fixtures/commands/manifest/gradle-facts/single-module-java/settings.gradle deleted file mode 100644 index 3eeb892e6..000000000 --- a/test/fixtures/commands/manifest/gradle-facts/single-module-java/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'single-module-java' diff --git a/test/fixtures/commands/manifest/gradle-facts/unresolved-deps/build.gradle b/test/fixtures/commands/manifest/gradle-facts/unresolved-deps/build.gradle deleted file mode 100644 index d1b9da529..000000000 --- a/test/fixtures/commands/manifest/gradle-facts/unresolved-deps/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -// Fixture for the "unresolvable dep" path. Pairs a real dep with one that -// will never resolve from Maven Central so we exercise both branches of the -// init script: -// - first-level resolved deps via `lenient.firstLevelModuleDependencies` -// - unresolved deps via `lenient.unresolvedModuleDependencies` -plugins { - id 'java-library' -} - -group = 'com.example.socket' -version = '1.0.0' - -repositories { - mavenCentral() -} - -dependencies { - api 'org.slf4j:slf4j-api:1.7.36' - implementation 'com.example.does-not-exist:fake-artifact:9.9.9' -} diff --git a/test/fixtures/commands/manifest/gradle-facts/unresolved-deps/settings.gradle b/test/fixtures/commands/manifest/gradle-facts/unresolved-deps/settings.gradle deleted file mode 100644 index af3e0f106..000000000 --- a/test/fixtures/commands/manifest/gradle-facts/unresolved-deps/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'unresolved-deps' From b774126c5d0aa22f3b8a47e56e7c7afda9a762ce Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Wed, 3 Jun 2026 10:30:46 +0200 Subject: [PATCH 2/5] refactor(manifest): split --configs into --include-configs/--exclude-configs Match the Coana CLI's split of `--configs` into `--include-configs` and `--exclude-configs` for `socket manifest {gradle,kotlin,scala,auto}`; both forward to Coana as `--include-configs` / `--exclude-configs`. `--include-configs` keeps the old include-only semantics; `--exclude-configs` skips matching configurations (applied after the include filter). The old `--configs` flag and its socket.json `configs` key are removed (beta; clean break, no deprecated alias). REA-507 --- src/commands/manifest/cmd-manifest-gradle.mts | 65 ++++++++++++----- .../manifest/cmd-manifest-gradle.test.mts | 8 ++- src/commands/manifest/cmd-manifest-kotlin.mts | 58 +++++++++++---- .../manifest/cmd-manifest-kotlin.test.mts | 8 ++- src/commands/manifest/cmd-manifest-scala.mts | 72 +++++++++++++------ .../manifest/cmd-manifest-scala.test.mts | 12 ++-- .../manifest/coana-manifest-facts.mts | 13 ++-- .../manifest/convert-gradle-to-facts.mts | 9 ++- .../manifest/convert-sbt-to-facts.mts | 9 ++- .../manifest/generate_auto_manifest.mts | 14 ++-- src/utils/socket-json.mts | 6 +- 11 files changed, 195 insertions(+), 79 deletions(-) diff --git a/src/commands/manifest/cmd-manifest-gradle.mts b/src/commands/manifest/cmd-manifest-gradle.mts index 43b2cb96f..eb3ac6eac 100644 --- a/src/commands/manifest/cmd-manifest-gradle.mts +++ b/src/commands/manifest/cmd-manifest-gradle.mts @@ -39,10 +39,15 @@ const config: CliCommandConfig = { description: 'Generate `pom.xml` manifest file(s) instead of the default Socket facts file (`.socket.facts.json`)', }, - configs: { + includeConfigs: { 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', + 'When generating facts: comma-separated glob patterns matched against Gradle configuration names (case-sensitive, `*` and `?` wildcards). Only configurations matching at least one pattern are resolved. e.g. `*CompileClasspath,*RuntimeClasspath`. Default: every resolvable configuration except AGP instrumented-test classpaths', + }, + excludeConfigs: { + type: 'string', + description: + 'When generating facts: comma-separated glob patterns; Gradle configurations matching any pattern are skipped (applied after --include-configs)', }, ignoreUnresolved: { type: 'boolean', @@ -69,8 +74,9 @@ const config: CliCommandConfig = { By default, emits a single \`.socket.facts.json\` describing the resolved dependency graph of the whole build, using gradle (preferably your local \`gradlew\`). An unresolved dependency is a fatal error. You can pass - --configs= to restrict resolution to matching - configurations (e.g. \`*CompileClasspath,*RuntimeClasspath\`), and + --include-configs / --exclude-configs (comma-separated glob patterns) to + control which configurations are resolved (e.g. + --include-configs=\`*CompileClasspath,*RuntimeClasspath\`), and --ignore-unresolved to warn on unresolved dependencies instead of failing. Pass --pom to instead generate \`pom.xml\` manifest files via gradle (one per @@ -133,7 +139,15 @@ async function run( sockJson?.defaults?.manifest?.gradle, ) - let { bin, configs, facts, gradleOpts, ignoreUnresolved, verbose } = cli.flags + let { + bin, + excludeConfigs, + facts, + gradleOpts, + ignoreUnresolved, + includeConfigs, + verbose, + } = cli.flags // Set defaults for any flag/arg that is not given. Check socket.json first. if (!bin) { @@ -183,12 +197,26 @@ 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) + if (includeConfigs === undefined) { + if (sockJson.defaults?.manifest?.gradle?.includeConfigs !== undefined) { + includeConfigs = sockJson.defaults?.manifest?.gradle?.includeConfigs + logger.info( + `Using default --include-configs from ${SOCKET_JSON}:`, + includeConfigs, + ) + } else { + includeConfigs = '' + } + } + if (excludeConfigs === undefined) { + if (sockJson.defaults?.manifest?.gradle?.excludeConfigs !== undefined) { + excludeConfigs = sockJson.defaults?.manifest?.gradle?.excludeConfigs + logger.info( + `Using default --exclude-configs from ${SOCKET_JSON}:`, + excludeConfigs, + ) } else { - configs = '' + excludeConfigs = '' } } if (ignoreUnresolved === undefined) { @@ -203,18 +231,18 @@ async function run( } } - // `--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.) + // `--include-configs`, `--exclude-configs`, and `--ignore-unresolved` only + // affect facts generation; the pom path 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['includeConfigs'] !== undefined || + cli.flags['excludeConfigs'] !== undefined || cli.flags['ignoreUnresolved'] !== undefined) ) { logger.warn( - 'The `--configs` and `--ignore-unresolved` options only apply with `--facts`; ignoring them.', + 'The `--include-configs`, `--exclude-configs`, and `--ignore-unresolved` options only apply when generating Socket facts (not with `--pom`); ignoring them.', ) } @@ -260,10 +288,11 @@ async function run( if (facts) { await convertGradleToFacts({ bin: String(bin), - configs: String(configs || ''), cwd, + excludeConfigs: String(excludeConfigs || ''), gradleOpts: parsedGradleOpts, ignoreUnresolved: Boolean(ignoreUnresolved), + includeConfigs: String(includeConfigs || ''), 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 dd5868027..464b34dff 100644 --- a/src/commands/manifest/cmd-manifest-gradle.test.mts +++ b/src/commands/manifest/cmd-manifest-gradle.test.mts @@ -24,18 +24,20 @@ 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 + --exclude-configs When generating facts: comma-separated glob patterns; Gradle configurations matching any pattern are skipped (applied after --include-configs) --facts Emit a Socket facts JSON file (\`.socket.facts.json\`) describing the resolved dependency graph. This is the default; pass \`--pom\` to generate \`pom.xml\` files instead --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) + --include-configs When generating facts: comma-separated glob patterns matched against Gradle configuration names (case-sensitive, \`*\` and \`?\` wildcards). Only configurations matching at least one pattern are resolved. e.g. \`*CompileClasspath,*RuntimeClasspath\`. Default: every resolvable configuration except AGP instrumented-test classpaths --pom Generate \`pom.xml\` manifest file(s) instead of the default Socket facts file (\`.socket.facts.json\`) --verbose Print debug messages By default, emits a single \`.socket.facts.json\` describing the resolved dependency graph of the whole build, using gradle (preferably your local \`gradlew\`). An unresolved dependency is a fatal error. You can pass - --configs= to restrict resolution to matching - configurations (e.g. \`*CompileClasspath,*RuntimeClasspath\`), and + --include-configs / --exclude-configs (comma-separated glob patterns) to + control which configurations are resolved (e.g. + --include-configs=\`*CompileClasspath,*RuntimeClasspath\`), and --ignore-unresolved to warn on unresolved dependencies instead of failing. Pass --pom to instead generate \`pom.xml\` manifest files via gradle (one per diff --git a/src/commands/manifest/cmd-manifest-kotlin.mts b/src/commands/manifest/cmd-manifest-kotlin.mts index 1411b01bb..4af771e59 100644 --- a/src/commands/manifest/cmd-manifest-kotlin.mts +++ b/src/commands/manifest/cmd-manifest-kotlin.mts @@ -44,10 +44,15 @@ const config: CliCommandConfig = { description: 'Generate `pom.xml` manifest file(s) instead of the default Socket facts file (`.socket.facts.json`)', }, - configs: { + includeConfigs: { 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', + 'When generating facts: comma-separated glob patterns matched against Gradle configuration names (case-sensitive, `*` and `?` wildcards). Only configurations matching at least one pattern are resolved. e.g. `*CompileClasspath,*RuntimeClasspath`. Default: every resolvable configuration except AGP instrumented-test classpaths', + }, + excludeConfigs: { + type: 'string', + description: + 'When generating facts: comma-separated glob patterns; Gradle configurations matching any pattern are skipped (applied after --include-configs)', }, ignoreUnresolved: { type: 'boolean', @@ -74,8 +79,9 @@ const config: CliCommandConfig = { By default, emits a single \`.socket.facts.json\` describing the resolved dependency graph of the whole build, using gradle (preferably your local \`gradlew\`). An unresolved dependency is a fatal error. You can pass - --configs= to restrict resolution to matching - configurations (e.g. \`*CompileClasspath,*RuntimeClasspath\`), and + --include-configs / --exclude-configs (comma-separated glob patterns) to + control which configurations are resolved (e.g. + --include-configs=\`*CompileClasspath,*RuntimeClasspath\`), and --ignore-unresolved to warn on unresolved dependencies instead of failing. Pass --pom to instead generate \`pom.xml\` manifest files via gradle (one per @@ -138,7 +144,15 @@ async function run( sockJson?.defaults?.manifest?.gradle, ) - let { bin, configs, facts, gradleOpts, ignoreUnresolved, verbose } = cli.flags + let { + bin, + excludeConfigs, + facts, + gradleOpts, + ignoreUnresolved, + includeConfigs, + verbose, + } = cli.flags // Set defaults for any flag/arg that is not given. Check socket.json first. if (!bin) { @@ -188,12 +202,26 @@ 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) + if (includeConfigs === undefined) { + if (sockJson.defaults?.manifest?.gradle?.includeConfigs !== undefined) { + includeConfigs = sockJson.defaults?.manifest?.gradle?.includeConfigs + logger.info( + `Using default --include-configs from ${SOCKET_JSON}:`, + includeConfigs, + ) + } else { + includeConfigs = '' + } + } + if (excludeConfigs === undefined) { + if (sockJson.defaults?.manifest?.gradle?.excludeConfigs !== undefined) { + excludeConfigs = sockJson.defaults?.manifest?.gradle?.excludeConfigs + logger.info( + `Using default --exclude-configs from ${SOCKET_JSON}:`, + excludeConfigs, + ) } else { - configs = '' + excludeConfigs = '' } } if (ignoreUnresolved === undefined) { @@ -208,13 +236,16 @@ async function run( } } + // `--include-configs`, `--exclude-configs`, and `--ignore-unresolved` only + // affect facts generation; the pom path has no equivalent knobs. if ( !facts && - (cli.flags['configs'] !== undefined || + (cli.flags['includeConfigs'] !== undefined || + cli.flags['excludeConfigs'] !== undefined || cli.flags['ignoreUnresolved'] !== undefined) ) { logger.warn( - 'The `--configs` and `--ignore-unresolved` options only apply with `--facts`; ignoring them.', + 'The `--include-configs`, `--exclude-configs`, and `--ignore-unresolved` options only apply when generating Socket facts (not with `--pom`); ignoring them.', ) } @@ -260,10 +291,11 @@ async function run( if (facts) { await convertGradleToFacts({ bin: String(bin), - configs: String(configs || ''), cwd, + excludeConfigs: String(excludeConfigs || ''), gradleOpts: parsedGradleOpts, ignoreUnresolved: Boolean(ignoreUnresolved), + includeConfigs: String(includeConfigs || ''), 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 11c3384b6..6a136e8c9 100644 --- a/src/commands/manifest/cmd-manifest-kotlin.test.mts +++ b/src/commands/manifest/cmd-manifest-kotlin.test.mts @@ -24,18 +24,20 @@ 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 + --exclude-configs When generating facts: comma-separated glob patterns; Gradle configurations matching any pattern are skipped (applied after --include-configs) --facts Emit a Socket facts JSON file (\`.socket.facts.json\`) describing the resolved dependency graph. This is the default; pass \`--pom\` to generate \`pom.xml\` files instead --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) + --include-configs When generating facts: comma-separated glob patterns matched against Gradle configuration names (case-sensitive, \`*\` and \`?\` wildcards). Only configurations matching at least one pattern are resolved. e.g. \`*CompileClasspath,*RuntimeClasspath\`. Default: every resolvable configuration except AGP instrumented-test classpaths --pom Generate \`pom.xml\` manifest file(s) instead of the default Socket facts file (\`.socket.facts.json\`) --verbose Print debug messages By default, emits a single \`.socket.facts.json\` describing the resolved dependency graph of the whole build, using gradle (preferably your local \`gradlew\`). An unresolved dependency is a fatal error. You can pass - --configs= to restrict resolution to matching - configurations (e.g. \`*CompileClasspath,*RuntimeClasspath\`), and + --include-configs / --exclude-configs (comma-separated glob patterns) to + control which configurations are resolved (e.g. + --include-configs=\`*CompileClasspath,*RuntimeClasspath\`), and --ignore-unresolved to warn on unresolved dependencies instead of failing. Pass --pom to instead generate \`pom.xml\` manifest files via gradle (one per diff --git a/src/commands/manifest/cmd-manifest-scala.mts b/src/commands/manifest/cmd-manifest-scala.mts index faffa6e2e..f5589e4fe 100644 --- a/src/commands/manifest/cmd-manifest-scala.mts +++ b/src/commands/manifest/cmd-manifest-scala.mts @@ -39,10 +39,15 @@ const config: CliCommandConfig = { description: 'Generate `pom.xml` manifest file(s) instead of the default Socket facts file (`.socket.facts.json`)', }, - configs: { + includeConfigs: { type: 'string', description: - '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', + 'When generating facts: comma-separated glob patterns matched against sbt configuration names (case-sensitive, `*` and `?` wildcards). Bare names act as exact-name filters. Only configurations matching at least one pattern are resolved. Default: compile,optional,provided,runtime,test', + }, + excludeConfigs: { + type: 'string', + description: + 'When generating facts: comma-separated glob patterns; sbt configurations matching any pattern are skipped (applied after --include-configs)', }, ignoreUnresolved: { type: 'boolean', @@ -77,10 +82,11 @@ const config: CliCommandConfig = { By default, emits a single \`.socket.facts.json\` describing the resolved dependency graph of the whole build. It reads dependency metadata only and never downloads artifacts; an unresolved dependency is a fatal error. You - can pass --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. + can pass --include-configs / --exclude-configs (comma-separated glob + patterns) to choose which sbt configurations to resolve (e.g. + --include-configs=\`compile,test\` for exact names or \`*Test*\` for + variants), and --ignore-unresolved to warn on unresolved dependencies + instead of failing the run. Pass --pom to instead generate a \`pom.xml\` via \`sbt makePom\` from your \`build.sbt\`. The xml is the dependency manifest (like a package.json for @@ -154,8 +160,17 @@ async function run( sockJson?.defaults?.manifest?.sbt, ) - let { bin, configs, facts, ignoreUnresolved, out, sbtOpts, stdout, verbose } = - cli.flags + let { + bin, + excludeConfigs, + facts, + ignoreUnresolved, + includeConfigs, + out, + sbtOpts, + stdout, + verbose, + } = cli.flags // Set defaults for any flag/arg that is not given. Check socket.json first. if (!bin) { @@ -186,12 +201,26 @@ async function run( facts = false } } - if (configs === undefined) { - if (sockJson.defaults?.manifest?.sbt?.configs !== undefined) { - configs = sockJson.defaults?.manifest?.sbt?.configs - logger.info(`Using default --configs from ${SOCKET_JSON}:`, configs) + if (includeConfigs === undefined) { + if (sockJson.defaults?.manifest?.sbt?.includeConfigs !== undefined) { + includeConfigs = sockJson.defaults?.manifest?.sbt?.includeConfigs + logger.info( + `Using default --include-configs from ${SOCKET_JSON}:`, + includeConfigs, + ) + } else { + includeConfigs = '' + } + } + if (excludeConfigs === undefined) { + if (sockJson.defaults?.manifest?.sbt?.excludeConfigs !== undefined) { + excludeConfigs = sockJson.defaults?.manifest?.sbt?.excludeConfigs + logger.info( + `Using default --exclude-configs from ${SOCKET_JSON}:`, + excludeConfigs, + ) } else { - configs = '' + excludeConfigs = '' } } if (ignoreUnresolved === undefined) { @@ -240,17 +269,19 @@ async function run( verbose = false } - // `--configs` and `--ignore-unresolved` only affect --facts; the pom path - // (`sbt makePom`) 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.) + // `--include-configs`, `--exclude-configs`, and `--ignore-unresolved` only + // affect facts generation; the pom path (`sbt makePom`) 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['includeConfigs'] !== undefined || + cli.flags['excludeConfigs'] !== undefined || cli.flags['ignoreUnresolved'] !== undefined) ) { logger.warn( - 'The `--configs` and `--ignore-unresolved` options only apply with `--facts`; ignoring them.', + 'The `--include-configs`, `--exclude-configs`, and `--ignore-unresolved` options only apply when generating Socket facts (not with `--pom`); ignoring them.', ) } @@ -311,9 +342,10 @@ async function run( if (facts) { await convertSbtToFacts({ bin: String(bin), - configs: String(configs || ''), cwd, + excludeConfigs: String(excludeConfigs || ''), ignoreUnresolved: Boolean(ignoreUnresolved), + includeConfigs: String(includeConfigs || ''), sbtOpts: parsedSbtOpts, verbose: Boolean(verbose), }) diff --git a/src/commands/manifest/cmd-manifest-scala.test.mts b/src/commands/manifest/cmd-manifest-scala.test.mts index c647265d1..1aead4fda 100644 --- a/src/commands/manifest/cmd-manifest-scala.test.mts +++ b/src/commands/manifest/cmd-manifest-scala.test.mts @@ -24,9 +24,10 @@ describe('socket manifest scala', async () => { Options --bin Location of sbt binary to use - --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 + --exclude-configs When generating facts: comma-separated glob patterns; sbt configurations matching any pattern are skipped (applied after --include-configs) --facts Emit a Socket facts JSON file (\`.socket.facts.json\`) describing the resolved dependency graph. This is the default; pass \`--pom\` to generate \`pom.xml\` files instead --ignore-unresolved With --facts: warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file) + --include-configs When generating facts: comma-separated glob patterns matched against sbt configuration names (case-sensitive, \`*\` and \`?\` wildcards). Bare names act as exact-name filters. Only configurations matching at least one pattern are resolved. Default: compile,optional,provided,runtime,test --out Path of output file; where to store the resulting manifest, see also --stdout --pom Generate \`pom.xml\` manifest file(s) instead of the default Socket facts file (\`.socket.facts.json\`) --sbt-opts Additional options to pass on to sbt, as per \`sbt --help\` @@ -36,10 +37,11 @@ describe('socket manifest scala', async () => { By default, emits a single \`.socket.facts.json\` describing the resolved dependency graph of the whole build. It reads dependency metadata only and never downloads artifacts; an unresolved dependency is a fatal error. You - can pass --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. + can pass --include-configs / --exclude-configs (comma-separated glob + patterns) to choose which sbt configurations to resolve (e.g. + --include-configs=\`compile,test\` for exact names or \`*Test*\` for + variants), and --ignore-unresolved to warn on unresolved dependencies + instead of failing the run. Pass --pom to instead generate a \`pom.xml\` via \`sbt makePom\` from your \`build.sbt\`. The xml is the dependency manifest (like a package.json for diff --git a/src/commands/manifest/coana-manifest-facts.mts b/src/commands/manifest/coana-manifest-facts.mts index 0813a8383..b90404159 100644 --- a/src/commands/manifest/coana-manifest-facts.mts +++ b/src/commands/manifest/coana-manifest-facts.mts @@ -20,19 +20,21 @@ export async function runCoanaManifestFacts({ bin, buildOpts, buildOptsFlag, - configs, cwd, ecosystem, + excludeConfigs, ignoreUnresolved, + includeConfigs, verbose, }: { bin: string buildOpts: string[] buildOptsFlag: '--gradle-opts' | '--sbt-opts' - configs: string cwd: string ecosystem: 'gradle' | 'sbt' + excludeConfigs: string ignoreUnresolved: boolean + includeConfigs: string verbose: boolean }): Promise { // `coana manifest ` emits `.socket.facts.json` by default; @@ -42,8 +44,11 @@ export async function runCoanaManifestFacts({ if (bin) { coanaArgs.push('--bin', bin) } - if (configs) { - coanaArgs.push('--configs', configs) + if (includeConfigs) { + coanaArgs.push('--include-configs', includeConfigs) + } + if (excludeConfigs) { + coanaArgs.push('--exclude-configs', excludeConfigs) } if (ignoreUnresolved) { coanaArgs.push('--ignore-unresolved') diff --git a/src/commands/manifest/convert-gradle-to-facts.mts b/src/commands/manifest/convert-gradle-to-facts.mts index a152521d9..31d5febb1 100644 --- a/src/commands/manifest/convert-gradle-to-facts.mts +++ b/src/commands/manifest/convert-gradle-to-facts.mts @@ -7,27 +7,30 @@ import { runCoanaManifestFacts } from './coana-manifest-facts.mts' // `./gradlew`. export async function convertGradleToFacts({ bin, - configs, cwd, + excludeConfigs, gradleOpts, ignoreUnresolved, + includeConfigs, verbose, }: { bin: string - configs: string cwd: string + excludeConfigs: string gradleOpts: string[] ignoreUnresolved: boolean + includeConfigs: string verbose: boolean }): Promise { await runCoanaManifestFacts({ bin, buildOpts: gradleOpts, buildOptsFlag: '--gradle-opts', - configs, cwd, ecosystem: 'gradle', + excludeConfigs, ignoreUnresolved, + includeConfigs, verbose, }) } diff --git a/src/commands/manifest/convert-sbt-to-facts.mts b/src/commands/manifest/convert-sbt-to-facts.mts index 2b4f9747e..4e6124c87 100644 --- a/src/commands/manifest/convert-sbt-to-facts.mts +++ b/src/commands/manifest/convert-sbt-to-facts.mts @@ -9,16 +9,18 @@ import { runCoanaManifestFacts } from './coana-manifest-facts.mts' // or `JAVA_HOME`. export async function convertSbtToFacts({ bin, - configs, cwd, + excludeConfigs, ignoreUnresolved, + includeConfigs, sbtOpts, verbose, }: { bin: string - configs: string cwd: string + excludeConfigs: string ignoreUnresolved: boolean + includeConfigs: string sbtOpts: string[] verbose: boolean }): Promise { @@ -26,10 +28,11 @@ export async function convertSbtToFacts({ bin, buildOpts: sbtOpts, buildOptsFlag: '--sbt-opts', - configs, cwd, ecosystem: 'sbt', + excludeConfigs, ignoreUnresolved, + includeConfigs, verbose, }) } diff --git a/src/commands/manifest/generate_auto_manifest.mts b/src/commands/manifest/generate_auto_manifest.mts index 22a8ee299..b870a7f28 100644 --- a/src/commands/manifest/generate_auto_manifest.mts +++ b/src/commands/manifest/generate_auto_manifest.mts @@ -37,9 +37,9 @@ export async function generateAutoManifest({ } if (!sockJson?.defaults?.manifest?.sbt?.disabled && detected.sbt) { - // Args shared by both paths. The facts-only knobs (`configs`, - // `ignoreUnresolved`) and the pom-only `out` are added per branch so - // neither handler is spread properties it doesn't accept. + // Args shared by both paths. The facts-only knobs (`includeConfigs`, + // `excludeConfigs`, `ignoreUnresolved`) and the pom-only `out` are added + // per branch so neither handler is spread properties it doesn't accept. const sbtArgs = { // Note: `sbt` is more likely to be resolved against PATH env. bin: sockJson.defaults?.manifest?.sbt?.bin ?? 'sbt', @@ -57,10 +57,11 @@ export async function generateAutoManifest({ logger.log('Detected a Scala sbt build, generating Socket facts...') await convertSbtToFacts({ ...sbtArgs, - configs: sockJson.defaults?.manifest?.sbt?.configs ?? '', + excludeConfigs: sockJson.defaults?.manifest?.sbt?.excludeConfigs ?? '', ignoreUnresolved: Boolean( sockJson.defaults?.manifest?.sbt?.ignoreUnresolved, ), + includeConfigs: sockJson.defaults?.manifest?.sbt?.includeConfigs ?? '', }) } else { logger.log('Detected a Scala sbt build, generating pom files with sbt...') @@ -95,10 +96,13 @@ export async function generateAutoManifest({ ) await convertGradleToFacts({ ...gradleArgs, - configs: sockJson.defaults?.manifest?.gradle?.configs ?? '', + excludeConfigs: + sockJson.defaults?.manifest?.gradle?.excludeConfigs ?? '', ignoreUnresolved: Boolean( sockJson.defaults?.manifest?.gradle?.ignoreUnresolved, ), + includeConfigs: + sockJson.defaults?.manifest?.gradle?.includeConfigs ?? '', }) } else { logger.log( diff --git a/src/utils/socket-json.mts b/src/utils/socket-json.mts index e6128ee61..5bbbb21d4 100644 --- a/src/utils/socket-json.mts +++ b/src/utils/socket-json.mts @@ -61,7 +61,8 @@ export interface SocketJson { gradle?: { disabled?: boolean | undefined bin?: string | undefined - configs?: string | undefined + excludeConfigs?: string | undefined + includeConfigs?: string | undefined facts?: boolean | undefined gradleOpts?: string | undefined ignoreUnresolved?: boolean | undefined @@ -72,7 +73,8 @@ export interface SocketJson { infile?: string | undefined stdin?: boolean | undefined bin?: string | undefined - configs?: string | undefined + excludeConfigs?: string | undefined + includeConfigs?: string | undefined facts?: boolean | undefined ignoreUnresolved?: boolean | undefined outfile?: string | undefined From 20a4efb399c6a814b357fc4c2129290c39ab46a4 Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Wed, 3 Jun 2026 10:41:59 +0200 Subject: [PATCH 3/5] docs(manifest): align sbt --include-configs wording with gradle Match the gradle flag description and help prose: drop the sbt-only "bare names act as exact-name filters" / "for variants" asides so both ecosystems document the include/exclude config flags identically as globs. --- src/commands/manifest/cmd-manifest-scala.mts | 9 ++++----- src/commands/manifest/cmd-manifest-scala.test.mts | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/commands/manifest/cmd-manifest-scala.mts b/src/commands/manifest/cmd-manifest-scala.mts index f5589e4fe..73d4f5533 100644 --- a/src/commands/manifest/cmd-manifest-scala.mts +++ b/src/commands/manifest/cmd-manifest-scala.mts @@ -42,7 +42,7 @@ const config: CliCommandConfig = { includeConfigs: { type: 'string', description: - 'When generating facts: comma-separated glob patterns matched against sbt configuration names (case-sensitive, `*` and `?` wildcards). Bare names act as exact-name filters. Only configurations matching at least one pattern are resolved. Default: compile,optional,provided,runtime,test', + 'When generating facts: comma-separated glob patterns matched against sbt configuration names (case-sensitive, `*` and `?` wildcards). Only configurations matching at least one pattern are resolved. e.g. `compile,test`. Default: compile,optional,provided,runtime,test', }, excludeConfigs: { type: 'string', @@ -83,10 +83,9 @@ const config: CliCommandConfig = { dependency graph of the whole build. It reads dependency metadata only and never downloads artifacts; an unresolved dependency is a fatal error. You can pass --include-configs / --exclude-configs (comma-separated glob - patterns) to choose which sbt configurations to resolve (e.g. - --include-configs=\`compile,test\` for exact names or \`*Test*\` for - variants), and --ignore-unresolved to warn on unresolved dependencies - instead of failing the run. + patterns) to control which sbt configurations are resolved (e.g. + --include-configs=\`compile,test\`), and --ignore-unresolved to warn on + unresolved dependencies instead of failing the run. Pass --pom to instead generate a \`pom.xml\` via \`sbt makePom\` from your \`build.sbt\`. The xml is the dependency manifest (like a package.json for diff --git a/src/commands/manifest/cmd-manifest-scala.test.mts b/src/commands/manifest/cmd-manifest-scala.test.mts index 1aead4fda..724e9e236 100644 --- a/src/commands/manifest/cmd-manifest-scala.test.mts +++ b/src/commands/manifest/cmd-manifest-scala.test.mts @@ -27,7 +27,7 @@ describe('socket manifest scala', async () => { --exclude-configs When generating facts: comma-separated glob patterns; sbt configurations matching any pattern are skipped (applied after --include-configs) --facts Emit a Socket facts JSON file (\`.socket.facts.json\`) describing the resolved dependency graph. This is the default; pass \`--pom\` to generate \`pom.xml\` files instead --ignore-unresolved With --facts: warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file) - --include-configs When generating facts: comma-separated glob patterns matched against sbt configuration names (case-sensitive, \`*\` and \`?\` wildcards). Bare names act as exact-name filters. Only configurations matching at least one pattern are resolved. Default: compile,optional,provided,runtime,test + --include-configs When generating facts: comma-separated glob patterns matched against sbt configuration names (case-sensitive, \`*\` and \`?\` wildcards). Only configurations matching at least one pattern are resolved. e.g. \`compile,test\`. Default: compile,optional,provided,runtime,test --out Path of output file; where to store the resulting manifest, see also --stdout --pom Generate \`pom.xml\` manifest file(s) instead of the default Socket facts file (\`.socket.facts.json\`) --sbt-opts Additional options to pass on to sbt, as per \`sbt --help\` @@ -38,10 +38,9 @@ describe('socket manifest scala', async () => { dependency graph of the whole build. It reads dependency metadata only and never downloads artifacts; an unresolved dependency is a fatal error. You can pass --include-configs / --exclude-configs (comma-separated glob - patterns) to choose which sbt configurations to resolve (e.g. - --include-configs=\`compile,test\` for exact names or \`*Test*\` for - variants), and --ignore-unresolved to warn on unresolved dependencies - instead of failing the run. + patterns) to control which sbt configurations are resolved (e.g. + --include-configs=\`compile,test\`), and --ignore-unresolved to warn on + unresolved dependencies instead of failing the run. Pass --pom to instead generate a \`pom.xml\` via \`sbt makePom\` from your \`build.sbt\`. The xml is the dependency manifest (like a package.json for From bc0fd8bc353b8549300ab1b74da70b77ad9269a0 Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Wed, 3 Jun 2026 12:48:35 +0200 Subject: [PATCH 4/5] feat(manifest): configure facts options in the setup wizard `socket manifest setup` now reflects Socket facts as the default for the gradle and sbt generators and lets you configure the facts-only options it previously couldn't: --include-configs, --exclude-configs, and --ignore-unresolved. These are prompted only when facts generation is selected (not --pom), and the sbt pom output questions (stdout/outfile) now only appear when pom is chosen. Refreshes the stale "generate pom.xml (default)" wording left over from the facts-by-default switch. --- .../manifest/setup-manifest-config.mts | 113 ++++++++++++++++-- 1 file changed, 103 insertions(+), 10 deletions(-) diff --git a/src/commands/manifest/setup-manifest-config.mts b/src/commands/manifest/setup-manifest-config.mts index 409a17399..c6cb97b0a 100644 --- a/src/commands/manifest/setup-manifest-config.mts +++ b/src/commands/manifest/setup-manifest-config.mts @@ -61,22 +61,24 @@ export async function setupManifestConfig( { name: 'Gradle'.padEnd(30, ' '), value: 'gradle', - description: 'Generate pom.xml files through gradle', + description: 'Generate a Socket facts file or pom.xml through gradle', }, { name: 'Kotlin (gradle)'.padEnd(30, ' '), value: 'gradle', - description: 'Generate pom.xml files (for Kotlin) through gradle', + description: + 'Generate a Socket facts file or pom.xml (for Kotlin) through gradle', }, { name: 'Scala (gradle)'.padEnd(30, ' '), value: 'gradle', - description: 'Generate pom.xml files (for Scala) through gradle', + description: + 'Generate a Socket facts file or pom.xml (for Scala) through gradle', }, { name: 'Scala (sbt)'.padEnd(30, ' '), value: 'sbt', - description: 'Generate pom.xml files through sbt', + description: 'Generate a Socket facts file or pom.xml through sbt', }, ] @@ -292,6 +294,15 @@ async function setupGradle( delete config.facts } + // The config filters and --ignore-unresolved only apply to facts generation + // (the default); skip them when pom generation (--pom) is selected. + if (config.facts !== false) { + const factsOptions = await setupFactsOptions(config) + if (!factsOptions.ok || factsOptions.data.canceled) { + return factsOptions + } + } + const verbose = await askForVerboseFlag(config.verbose) if (verbose === undefined) { return canceledByUser() @@ -341,9 +352,10 @@ async function setupSbt( delete config.facts } - // --facts emits a .socket.facts.json instead of pom.xml files, so the pom - // output questions (stdout/outfile) don't apply when it is enabled. - if (config.facts !== true) { + // Socket facts is the default. The pom output questions (stdout/outfile) + // only apply when pom generation (--pom) is explicitly selected; otherwise + // ask the facts-only options. + if (config.facts === false) { const stdout = await askForStdout(config.stdout) if (stdout === undefined) { return canceledByUser() @@ -370,6 +382,11 @@ async function setupSbt( } } } + } else { + const factsOptions = await setupFactsOptions(config) + if (!factsOptions.ok || factsOptions.data.canceled) { + return factsOptions + } } const verbose = await askForVerboseFlag(config.verbose) @@ -504,21 +521,48 @@ async function askForVerboseFlag( async function askForFactsFlag( current: boolean | undefined, +): Promise { + return await select({ + message: '(--facts / --pom) Which manifest should this generate?', + choices: [ + { + name: 'Socket facts (default)', + value: 'yes', + description: + 'Generate a .socket.facts.json file describing the resolved dependency graph', + }, + { + name: 'pom.xml', + value: 'no', + description: 'Generate pom.xml manifest files instead (the --pom path)', + }, + { + name: '(leave default)', + value: '', + description: 'Do not store a setting; uses the default (Socket facts)', + }, + ], + default: current === true ? 'yes' : current === false ? 'no' : '', + }) +} + +async function askForIgnoreUnresolvedFlag( + current: boolean | undefined, ): Promise { return await select({ message: - '(--facts) Emit a Socket facts JSON file instead of generating pom.xml?', + '(--ignore-unresolved) Warn on unresolved dependencies instead of failing?', choices: [ { name: 'no', value: 'no', - description: 'Generate pom.xml files (default behavior)', + description: 'Fail the run when a declared dependency cannot resolve', }, { name: 'yes', value: 'yes', description: - 'Generate a .socket.facts.json file describing the resolved dependency graph', + 'Warn and continue; unresolved dependencies are omitted from the facts file', }, { name: '(leave default)', @@ -530,6 +574,55 @@ async function askForFactsFlag( }) } +// Prompts for the facts-only options shared by gradle and sbt: the config +// include/exclude filters and --ignore-unresolved. Mutates `config` in place. +async function setupFactsOptions(config: { + excludeConfigs?: string | undefined + ignoreUnresolved?: boolean | undefined + includeConfigs?: string | undefined +}): Promise> { + const includeConfigs = await input({ + message: + '(--include-configs) Comma-separated config-name globs to resolve (blank = all configurations)', + default: config.includeConfigs || '', + required: false, + }) + if (includeConfigs === undefined) { + return canceledByUser() + } else if (includeConfigs) { + config.includeConfigs = includeConfigs + } else { + delete config.includeConfigs + } + + const excludeConfigs = await input({ + message: + '(--exclude-configs) Comma-separated config-name globs to skip (blank = none)', + default: config.excludeConfigs || '', + required: false, + }) + if (excludeConfigs === undefined) { + return canceledByUser() + } else if (excludeConfigs) { + config.excludeConfigs = excludeConfigs + } else { + delete config.excludeConfigs + } + + const ignoreUnresolved = await askForIgnoreUnresolvedFlag( + config.ignoreUnresolved, + ) + if (ignoreUnresolved === undefined) { + return canceledByUser() + } else if (ignoreUnresolved === 'yes' || ignoreUnresolved === 'no') { + config.ignoreUnresolved = ignoreUnresolved === 'yes' + } else { + delete config.ignoreUnresolved + } + + return notCanceled() +} + function canceledByUser(): CResult<{ canceled: boolean }> { logger.log('') logger.info('User canceled') From acdba639e02f96b6711215673605667d46bd9fee Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Wed, 3 Jun 2026 12:54:19 +0200 Subject: [PATCH 5/5] chore: cut 1.1.113 and bump @coana-tech/cli to 15.3.19 Finalizes the manifest facts-by-default / Coana-delegation work: bumps the socket-cli version to 1.1.113 and pins @coana-tech/cli to the published 15.3.19 (which ships the `manifest gradle|sbt` commands this PR delegates to), plus the 1.1.113 CHANGELOG entry. REA-507 --- CHANGELOG.md | 7 +++++++ package.json | 4 ++-- pnpm-lock.yaml | 10 +++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c86f24e2..501f5d344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,13 @@ 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.113](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.113) - 2026-06-03 + +### Changed +- `socket manifest gradle`, `kotlin`, and `scala` now generate a Socket facts file (`.socket.facts.json`) by default; pass `--pom` to generate `pom.xml` manifests instead. +- Replaced `--configs` with `--include-configs` and `--exclude-configs` on `socket manifest gradle/kotlin/scala` for finer control over which build configurations are resolved. +- Updated the Coana CLI to v `15.3.19`. + ## [1.1.112](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.112) - 2026-05-29 ### Fixed diff --git a/package.json b/package.json index 862d49138..4ba3a44bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket", - "version": "1.1.112", + "version": "1.1.113", "description": "CLI for Socket.dev", "homepage": "https://github.com/SocketDev/socket-cli", "license": "MIT AND OFL-1.1", @@ -96,7 +96,7 @@ "@babel/preset-typescript": "7.27.1", "@babel/runtime": "7.28.4", "@biomejs/biome": "2.2.4", - "@coana-tech/cli": "15.3.15", + "@coana-tech/cli": "15.3.19", "@cyclonedx/cdxgen": "12.1.2", "@dotenvx/dotenvx": "1.49.0", "@eslint/compat": "1.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 930ab0914..630b82b99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,8 +128,8 @@ importers: specifier: 2.2.4 version: 2.2.4 '@coana-tech/cli': - specifier: 15.3.15 - version: 15.3.15 + specifier: 15.3.19 + version: 15.3.19 '@cyclonedx/cdxgen': specifier: 12.1.2 version: 12.1.2 @@ -749,8 +749,8 @@ packages: resolution: {integrity: sha512-hAs5PPKPCQ3/Nha+1fo4A4/gL85fIfxZwHPehsjCJ+BhQH2/yw6/xReuaPA/RfNQr6iz1PcD7BZcE3ctyyl3EA==} cpu: [x64] - '@coana-tech/cli@15.3.15': - resolution: {integrity: sha512-AH6my6LNU61JpP52NAdiYYRlZFGEnRngDAt8yKrLDgnPeuiVWkWnYequcEiPX9RFvhxKO/eW9PN14K/ixdsfkQ==} + '@coana-tech/cli@15.3.19': + resolution: {integrity: sha512-X7rIj95G9gOUoQVh2C9/dUQR5ify+OE9xoSulxAyM+NASo92dD0wB1ooLAR8+DXdQV7XOQzUKEoQvWQXhiuMeA==} hasBin: true '@colors/colors@1.5.0': @@ -5385,7 +5385,7 @@ snapshots: '@cdxgen/cdxgen-plugins-bin@2.0.2': optional: true - '@coana-tech/cli@15.3.15': {} + '@coana-tech/cli@15.3.19': {} '@colors/colors@1.5.0': optional: true