Skip to content

Commit ae775fb

Browse files
feat(v5): implement vendored Maven reactors and Gradle builds (#287)
* Add the vendored Maven and Gradle design Vendored Maven today works for a single pom.xml only. It refuses multi-module reactors and Gradle builds, which covers most enterprise Java. This design says how vendored mode should handle both without dropping any Maven or Gradle version that works today. - Maven uses the server's suffixed version (<v>-socket.<hex8>), found through a committed maven2 tree. Maven 3.9.2+ reads it through maven.repo.local.tail; every version also gets a file:// fallback repository and dependencyManagement pins in each local root pom. - Gradle keeps the same coordinates. One owned settings script, applied by one line, adds an exclusiveContent file repository and checks the vendored files' sha256 on every configuration. Lockfiles, catalogs and build.gradle files are never edited. - Build-time Maven pins (trusted checksums, deny lines) are opt-in for later. The adversarial review found crashes and silent no-ops in them on common Maven versions. The doc includes the supported-shapes matrix, the failure modes, the refusal-code mapping, the migration plan, the server/CLI split, the test plan, the panel scores and the adversarial review findings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZLLAZfofQZ2ywkgrHkdEZ * Drop unused rollback fixture hash fields Workspace clippy with -D warnings failed on two fields of the covgap_commands_rollback fixture that nothing reads. The hashes are still computed and used to stage the manifest and blobs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZLLAZfofQZ2ywkgrHkdEZ * Prototype vendored Maven reactors and Gradle Vendored Maven refuses multi-module reactors and Gradle builds today. With SOCKET_PATCH_EXPERIMENTAL_JVM_VENDOR=1 set, those two shapes now go to a new backend (vendor/jvm) that implements the v5.0 cut of docs/design/maven-vendoring.md. With the variable unset, nothing that vendors today behaves differently. - Maven reactor: pins the server's suffixed version in every local root pom and rewrites literal and property declarations (including in profiles and middle parents). Adds one shared file:// fallback repository, two .mvn/maven.config lines (offline file protocol and maven.repo.local.tail) and a committed maven2 tree. - Gradle: keeps the same coordinates. One apply line per settings file (root, buildSrc, literal includeBuild) loads an owned script. It adds an exclusiveContent file repository and fails configuration if a vendored file's sha256 no longer matches the index. Lockfiles, catalogs and build scripts are never edited; an existing verification-metadata.xml gets the patched hash. - Revert is per fragment and byte-exact in any order. Shared pieces stay until no other patch uses them. Re-runs, patch updates, --preserve-state, the cold-cache fast path, VEX liveness and repair all handle the new trees. The real-tool capstones (e2e_vendor_jvm_build, ignored by default) build a reactor and a locked Kotlin DSL Gradle build offline from a fresh checkout. They pass on Maven 3.8.8 to 4.0.0-rc-7 and Gradle 6.9.4 to 9.8.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZLLAZfofQZ2ywkgrHkdEZ * Keep rollback fixture hashes for the macOS test The previous commit dropped two fixture fields to quiet clippy on Linux. A macOS-only test reads them, so the macOS test build broke. Restore the fields and allow dead code on other platforms instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZLLAZfofQZ2ywkgrHkdEZ * Check out the Gradle settings script byte for byte The owned Gradle script is embedded with include_str! and written into user repos as is. A Windows checkout with autocrlf turned it into CRLF, so Windows builds would write different script bytes (and the script sanity test failed there). Mark it -text like the other byte-exact files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZLLAZfofQZ2ywkgrHkdEZ * feat: complete v5 Maven reactor and Gradle vendoring * test: close v5 validation gaps and require JVM toolchains * test: align v5 help and canonical restore goldens * test: make JVM build capstones portable on Windows * ci: extract Maven without interpreting Windows drives as hosts * ci: use native-path Maven extraction and parallelize build checks * test: normalize paths in Gradle integrity diagnostics --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 785fbb0 commit ae775fb

51 files changed

Lines changed: 12587 additions & 60 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitattributes‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,7 @@ crates/socket-patch-core/tests/fixtures/vendor/** -text
2929
# compares the result byte for byte, so a CRLF checkout would change both
3030
# the replayed wiring files and the expected revert.
3131
crates/socket-patch-cli/tests/fixtures/legacy-ledgers/** -text
32+
33+
# The owned Gradle settings script is embedded with include_str! and
34+
# written into user repos byte for byte; a CRLF checkout would change it.
35+
crates/socket-patch-core/src/vendor/jvm/socket-patch.settings.gradle -text

‎.github/workflows/ci.yml‎

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,8 @@ jobs:
729729
retention-days: 3
730730

731731
e2e:
732-
needs: [test, e2e-build]
732+
# These jobs consume e2e-build's binaries and can run alongside unit tests.
733+
needs: [e2e-build]
733734
strategy:
734735
fail-fast: false
735736
matrix:
@@ -1069,6 +1070,18 @@ jobs:
10691070
- {os: ubuntu-latest, suite: e2e_vendor_maven_build, maven: '3.9.16'}
10701071
- {os: ubuntu-latest, suite: e2e_vendor_maven_build, maven: '4.0.0-rc-6'}
10711072
- {os: macos-latest, suite: e2e_vendor_maven_build, maven: '3.9.16'}
1073+
- {os: ubuntu-latest, suite: e2e_vendor_jvm_build, maven: '3.6.3', test_filter: '--ignored maven_reactor'}
1074+
- {os: ubuntu-latest, suite: e2e_vendor_jvm_build, maven: '3.8.9', test_filter: '--ignored maven_reactor'}
1075+
- {os: ubuntu-latest, suite: e2e_vendor_jvm_build, maven: '3.9.2', test_filter: '--ignored maven_reactor'}
1076+
- {os: ubuntu-latest, suite: e2e_vendor_jvm_build, maven: '3.9.16', test_filter: '--ignored maven_reactor'}
1077+
- {os: ubuntu-latest, suite: e2e_vendor_jvm_build, maven: '4.0.0-rc-6', test_filter: '--ignored maven_reactor'}
1078+
- {os: macos-latest, suite: e2e_vendor_jvm_build, maven: '3.9.16', test_filter: '--ignored maven_reactor'}
1079+
- {os: windows-latest, suite: e2e_vendor_jvm_build, maven: '3.9.16', test_filter: '--ignored maven_reactor'}
1080+
- {os: ubuntu-latest, suite: e2e_vendor_jvm_build, gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_multi_project'}
1081+
- {os: ubuntu-latest, suite: e2e_vendor_jvm_build, gradle: '7.6.4', java: '17', test_filter: '--ignored gradle_multi_project'}
1082+
- {os: ubuntu-latest, suite: e2e_vendor_jvm_build, gradle: '8.14.3', java: '17', test_filter: '--ignored gradle_multi_project'}
1083+
- {os: ubuntu-latest, suite: e2e_vendor_jvm_build, gradle: '9.8.0', java: '17', test_filter: '--ignored gradle_multi_project'}
1084+
- {os: windows-latest, suite: e2e_vendor_jvm_build, gradle: '8.14.3', java: '17', test_filter: '--ignored gradle_multi_project'}
10721085
# Real .NET SDK capstones: hosted + vendored nuget, one leg per SDK
10731086
# major (the suite pins the major through a sandbox global.json):
10741087
# the oldest and newest here, 7-9 on ubuntu in e2e-full.
@@ -1238,28 +1251,46 @@ jobs:
12381251
php-version: '8.2'
12391252
tools: composer:${{ matrix.composer }}
12401253

1241-
- name: Setup Java (Maven legs)
1242-
if: matrix.maven != ''
1254+
- name: Setup Java (Maven and Gradle legs)
1255+
if: matrix.maven != '' || matrix.gradle != ''
12431256
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
12441257
with:
12451258
distribution: temurin
1246-
java-version: '17'
1259+
java-version: ${{ matrix.java || '17' }}
12471260

1248-
- name: Install Maven ${{ matrix.maven }}
1249-
if: matrix.maven != ''
1261+
- name: Install Maven ${{ matrix.maven || '3.9.16' }}
1262+
if: matrix.maven != '' || matrix.gradle != ''
12501263
# Straight from the Apache archive (sha512-verified), so a leg gets
12511264
# exactly the release it names rather than the runner's Maven.
12521265
shell: bash
12531266
env:
1254-
MAVEN_VERSION: ${{ matrix.maven }}
1267+
MAVEN_VERSION: ${{ matrix.maven || '3.9.16' }}
12551268
run: |
12561269
major="${MAVEN_VERSION%%.*}"
12571270
url="https://archive.apache.org/dist/maven/maven-${major}/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz"
12581271
curl -fsSL --retry 3 "$url" -o "$RUNNER_TEMP/maven.tgz"
1259-
sum="$(curl -fsSL --retry 3 "$url.sha512" | cut -d' ' -f1)"
1260-
echo "$sum $RUNNER_TEMP/maven.tgz" | shasum -a 512 -c -
1261-
tar -xzf "$RUNNER_TEMP/maven.tgz" -C "$RUNNER_TEMP"
1262-
echo "SOCKET_PATCH_MAVEN_E2E_MVN=$RUNNER_TEMP/apache-maven-${MAVEN_VERSION}/bin/mvn" >> "$GITHUB_ENV"
1272+
curl -fsSL --retry 3 "$url.sha512" -o "$RUNNER_TEMP/maven.sha512"
1273+
python -c 'import hashlib, pathlib, os; p=pathlib.Path(os.environ["RUNNER_TEMP"]); assert hashlib.sha512((p/"maven.tgz").read_bytes()).hexdigest() == (p/"maven.sha512").read_text().split()[0]'
1274+
# Python accepts native Windows paths for both archive and destination.
1275+
python -m tarfile -e "$RUNNER_TEMP/maven.tgz" "$RUNNER_TEMP"
1276+
launcher="$RUNNER_TEMP/apache-maven-${MAVEN_VERSION}/bin/mvn"
1277+
if [ "$RUNNER_OS" = Windows ]; then launcher="${launcher}.cmd"; fi
1278+
echo "SOCKET_PATCH_MAVEN_E2E_MVN=$launcher" >> "$GITHUB_ENV"
1279+
1280+
- name: Install Gradle ${{ matrix.gradle }}
1281+
if: matrix.gradle != ''
1282+
shell: bash
1283+
env:
1284+
GRADLE_VERSION: ${{ matrix.gradle }}
1285+
run: |
1286+
url="https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip"
1287+
curl -fsSL --retry 3 "$url" -o "$RUNNER_TEMP/gradle.zip"
1288+
curl -fsSL --retry 3 "$url.sha256" -o "$RUNNER_TEMP/gradle.sha256"
1289+
python -c 'import hashlib, pathlib, os; p=pathlib.Path(os.environ["RUNNER_TEMP"]); assert hashlib.sha256((p/"gradle.zip").read_bytes()).hexdigest() == (p/"gradle.sha256").read_text().strip()'
1290+
unzip -q "$RUNNER_TEMP/gradle.zip" -d "$RUNNER_TEMP"
1291+
launcher="$RUNNER_TEMP/gradle-${GRADLE_VERSION}/bin/gradle"
1292+
if [ "$RUNNER_OS" = Windows ]; then launcher="${launcher}.bat"; fi
1293+
echo "SOCKET_PATCH_GRADLE_E2E_GRADLE=$launcher" >> "$GITHUB_ENV"
12631294
12641295
- name: Setup .NET SDK
12651296
if: matrix.dotnet != ''
@@ -1363,8 +1394,10 @@ jobs:
13631394
SOCKET_PATCH_BUNDLER_E2E_VERSION: ${{ matrix.bundler }}
13641395
SOCKET_PATCH_COMPOSER_E2E_REQUIRED: ${{ matrix.composer != '' && '1' || '' }}
13651396
SOCKET_PATCH_COMPOSER_E2E_VERSION: ${{ matrix.composer }}
1366-
SOCKET_PATCH_MAVEN_E2E_REQUIRED: ${{ matrix.maven != '' && '1' || '' }}
1367-
SOCKET_PATCH_MAVEN_E2E_VERSION: ${{ matrix.maven }}
1397+
SOCKET_PATCH_MAVEN_E2E_REQUIRED: ${{ (matrix.maven != '' || matrix.gradle != '') && '1' || '' }}
1398+
SOCKET_PATCH_MAVEN_E2E_VERSION: ${{ matrix.maven || (matrix.gradle != '' && '3.9.16') || '' }}
1399+
SOCKET_PATCH_GRADLE_E2E_REQUIRED: ${{ matrix.gradle != '' && '1' || '' }}
1400+
SOCKET_PATCH_GRADLE_E2E_VERSION: ${{ matrix.gradle }}
13681401
SOCKET_PATCH_DOTNET_E2E_REQUIRED: ${{ matrix.dotnet != '' && '1' || '' }}
13691402
SOCKET_PATCH_DOTNET_E2E_VERSION: ${{ matrix.dotnet }}
13701403
SOCKET_PATCH_DENO_E2E_REQUIRED: ${{ matrix.deno != '' && '1' || '' }}

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ and `vendor` (committed patched packages), with `list` for inspection. See the
7070

7171
### Added
7272

73+
- Vendored Maven reactors and Gradle builds, with committed repositories,
74+
reversible wiring, repair, rollback, and VEX. Reactors use suffixed versions;
75+
Gradle preserves coordinates and lockfiles, checks artifact hashes, and updates
76+
existing verification metadata. `vendor --check` audits artifacts and wiring
77+
offline; `--local-repo` checks Maven cache conflicts and `--maven-config=none`
78+
selects the fallback file repository. Single-POM vendoring is unchanged.
7379
- `socket.yml` patch policy for paths, ecosystems, packages, severity, and per-run
7480
limits. `scan --package`, `--min-severity`, `--max-new-patches`, and
7581
`--no-socket-yml` support targeted and gradual rollout. Already-patched packages

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ The CLI supports npm, PyPI, Cargo, Go, RubyGems, Maven, Composer, NuGet, and Den
102102
Mode and package-manager support vary: Deno uses agent mode, for example. Check the
103103
[ecosystem support matrix](docs/ecosystems.md) before choosing a mode.
104104

105+
Vendored Maven reactors and Gradle 6.8+ builds are supported. See
106+
[JVM vendoring](docs/design/maven-vendoring.md) for supported project shapes,
107+
cache behavior, and offline checks.
108+
105109
## Common commands
106110

107111
```sh

‎crates/socket-patch-cli/CLI_CONTRACT.md‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Rows are in `--help` order (v5.0): the hosted/vendored workflow (`scan` → `vex
4242

4343
## Global arguments
4444

45-
Every subcommand accepts the same set of "global" flags via a single shared `GlobalArgs` struct that's `#[command(flatten)]`-ed into each per-command struct (`crates/socket-patch-cli/src/args.rs`). Subcommands that don't actually consume a given flag accept it silently — e.g. `list --global` parses fine and is a no-op. Every flag also has an environment-variable binding; precedence is **CLI arg > env var > default** — and for exactly three keys (`--api-token`, `--org`, `--api-url`) the JS socket-cli's persisted login sits between env var and default: **CLI arg > env var (canonical, then `SOCKET_CLI_*` alias) > socket-cli `config.json` > default**. See "Persisted configuration" under Environment variables.
45+
Every subcommand accepts the same set of "global" flags via a single shared `GlobalArgs` struct that's `#[command(flatten)]`-ed into each per-command struct (`crates/socket-patch-cli/src/args.rs`). Subcommands that don't actually consume a given flag accept it silently — e.g. `list --global` parses fine and is a no-op. For flags with an environment-variable binding, precedence is **CLI arg > env var > default** — and for exactly three keys (`--api-token`, `--org`, `--api-url`) the JS socket-cli's persisted login sits between env var and default: **CLI arg > env var (canonical, then `SOCKET_CLI_*` alias) > socket-cli `config.json` > default**. See "Persisted configuration" under Environment variables.
4646

4747
| Long | Short | Env var | Default | Type | Semantic |
4848
|---|---|---|---|---|---|
@@ -55,6 +55,7 @@ Every subcommand accepts the same set of "global" flags via a single shared `Glo
5555
| `--ecosystems` | `-e` | `SOCKET_ECOSYSTEMS` | (all) | CSV → `Vec<String>` | Restrict to these ecosystems |
5656
| `--download-mode` | — | `SOCKET_DOWNLOAD_MODE` | **`diff`** | enum: `diff` \| `file` (`package` was removed and is rejected) | Patch artifact format |
5757
| `--vendor-source` | — | `SOCKET_VENDOR_SOURCE` | **`auto`** | enum: `auto` \| `service` \| `build` | How `vendor` acquires the installable artifact (see "Prebuilt vendor artifacts") |
58+
| `--maven-config` | — | — | (recorded choice, else `auto`) | enum: `auto` \| `none` | Maven reactor vendoring: write the repository tail (`auto`) or use only the fallback file repository (`none`). The choice persists in the vendor ledger. |
5859
| `--vendor-url` | — | `SOCKET_VENDOR_URL` | (active API/proxy base) | string | Base host for the vendoring-service package-reference request |
5960
| `--patch-server-url` | — | `SOCKET_PATCH_SERVER_URL` | (server-returned) | string | Override the host of the prebuilt-archive download URL (local-dev / testing) |
6061
| `--offline` | — | `SOCKET_OFFLINE` | `false` | bool | **Strict airgap on every command** — never contact the network |
@@ -87,6 +88,8 @@ Beyond the globals above, each subcommand defines a small set of local arguments
8788
| `apply` | `--check` | — | Read-only audit that the committed **Go** `replace`-redirects match the manifest (CI / GitHub-App auditing) — Go ONLY (cargo patches in place, so there is no redirect to audit). Lock-free, crawl-free, offline-safe; exits 0 in sync, 1 on drift. Vendored modules are excluded from the audit |
8889
| `vendor` | `--force` / `-f` | `SOCKET_FORCE` | Tolerate missing patch-target files in the stage + bypass the variant probe. A beforeHash mismatch no longer needs it: vendor staging auto-overwrites with the verified patched content (`vendor_content_mismatch_overwritten` warning) |
8990
| `vendor` | `--revert` | `SOCKET_VENDOR_REVERT` | Undo vendoring: restore recorded original lockfile fragments + remove `.socket/vendor/` artifacts. Works without a manifest. A package vendored over a hosted pin returns to its upstream registry entry, never to hosted (see "Takeover reconciliation") |
91+
| `vendor` | `--check` | — | Offline, read-only artifact and wiring audit; exits 1 on drift. Conflicts with `--revert`. |
92+
| `vendor` | `--local-repo <path>` | — | With `--check`, also inspect suffixed Maven jar/POM copies in this cache for conflicts. |
9093
| `apply`, `scan`, `vendor` | `--vex` | `SOCKET_VEX` | Generate an OpenVEX 0.2.0 document at this path on a successful run; see "embedded VEX" below |
9194
| `apply`, `scan`, `vendor` | `--vex-product`, `--vex-no-verify`, `--vex-doc-id`, `--vex-compact` | `SOCKET_VEX_PRODUCT`, `SOCKET_VEX_NO_VERIFY`, `SOCKET_VEX_DOC_ID`, `SOCKET_VEX_COMPACT` | Passthrough to the embedded VEX builder; mirror the standalone `vex` knobs. Inert unless `--vex` is set |
9295
| `scan` | positional `[PATHS]...` | — | (v5.0) Meaning depends on the mode. **Hosted / vendored** (bare `scan` included): each PATH, or directory glob (`apps/*`), is a project directory scanned on its own as if it were `--cwd`. **Agent** (and a mode-less `--prune`/`--global` report): path globs scoping DISCOVERY to packages installed under matching paths (`packages/foo`, `apps/**`). See "Path-scoped scans" below |
@@ -1722,3 +1725,24 @@ Every item in this document is locked in by at least one of:
17221725
- **Async `run()` integration tests** in `tests/cli_parse_list.rs`, `tests/cli_parse_remove.rs` — exercise the no-network error paths and assert JSON shape via `serde_json::from_str::<Value>` + per-key assertions.
17231726

17241727
If you add a new flag/subcommand/JSON key, add a test here that locks the new surface in the same PR.
1728+
1729+
1730+
### Vendored JVM support (v5)
1731+
1732+
Maven reactors and Gradle 6.8+ route to the JVM backend automatically. Ledger
1733+
entries use ecosystem `jvm` with Maven PURLs. Revert, remove, rollback and repair
1734+
share the v5 vendored backend; existing prototype wiring remains readable.
1735+
See [the JVM design](../../docs/design/maven-vendoring.md) for supported shapes.
1736+
1737+
`vendor --check` is an offline, read-only audit. Healthy entries emit `verified`
1738+
with `vendor_check_ok`; drift emits `failed` with `vendor_check_failed`, a
1739+
`partialFailure` envelope and exit 1. Missing ledger entries fail with
1740+
`vendor_ledger_missing`. Offline upstream metadata is reported as the run warning
1741+
`vendor_jvm_upstream_unverified`. The check never starts an API client or writes
1742+
lock/recovery files. `--check` conflicts with `--revert`.
1743+
1744+
`vendor --check --local-repo <path>` additionally checks existing suffixed Maven
1745+
jar/POM copies for conflicting bytes. `--maven-config auto|none` is a global
1746+
vendoring option so scan/get/repair receive it too; omission preserves the
1747+
ledger's recorded choice. Switching existing auto-config wiring to `none`
1748+
requires reverting it first. `none` cannot be combined with a repository ban.

‎crates/socket-patch-cli/src/args.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ pub struct GlobalArgs {
176176
)]
177177
pub vendor_source: String,
178178

179+
/// Vendored Maven: auto writes the repository tail; none uses only the file repository.
180+
/// The choice is preserved on subsequent runs.
181+
#[arg(help_heading = GLOBAL_OPTIONS, long, value_parser = ["auto", "none"])]
182+
pub maven_config: Option<String>,
183+
179184
/// Base URL for the patch vendoring service. Defaults to the active API base (`--api-url`) when
180185
/// authenticated or the proxy base (`--proxy-url`) otherwise. Override to
181186
/// point `vendor` at staging / local dev independently of `--api-url`.
@@ -507,6 +512,7 @@ impl GlobalArgs {
507512
use_public_proxy: bool,
508513
) -> VendorServiceConfig {
509514
VendorServiceConfig {
515+
maven_config: self.maven_config.as_deref().map(|v| v != "none"),
510516
source: VendorSource::parse(&self.vendor_source).unwrap_or_default(),
511517
client,
512518
use_public_proxy,
@@ -675,6 +681,7 @@ impl Default for GlobalArgs {
675681
ecosystems: None,
676682
download_mode: "diff".to_string(),
677683
vendor_source: "auto".to_string(),
684+
maven_config: None,
678685
vendor_url: None,
679686
patch_server_url: None,
680687
offline: false,

0 commit comments

Comments
 (0)