From 9b5c5491e6d43d5b205710025bcb97cb1c9a0a06 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 07:27:34 +0000 Subject: [PATCH 1/3] Bench: add gradle hosted and rescan scenarios #646 gave Gradle builds a hosted mode: scan crawls Gradle's modules-2/files-2.1 cache, pins suffixed versions in gradle.lockfile and wires the build through an owned settings script and index under .socket/gradle/. None of that was benchmarked; the maven scenarios only reach the pom.xml + ~/.m2 path. The gradle fixture is a single-project Groovy build with dependency locking (1000 locked artifacts, 25 patched direct deps), its cache under the fixture's GRADLE_USER_HOME with jar and pom in separate sha1 dirs. The Maven-coordinate generator, pom writer and maven2 grant builder are shared with the maven fixture, whose bytes are unchanged. The grant's indexUrl is https because the Gradle planner refuses anything else; scan never fetches it. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/socket-patch-bench/README.md | 11 +- crates/socket-patch-bench/src/fixtures/mod.rs | 7 + .../socket-patch-bench/src/fixtures/other.rs | 211 ++++++++++++++---- 3 files changed, 181 insertions(+), 48 deletions(-) diff --git a/crates/socket-patch-bench/README.md b/crates/socket-patch-bench/README.md index 51028a4d9..fe94f6cf9 100644 --- a/crates/socket-patch-bench/README.md +++ b/crates/socket-patch-bench/README.md @@ -39,8 +39,10 @@ layout: `npm`, `pnpm` (isolated `.pnpm` store with symlinks), `yarn-classic`, `bun-isolated` (the same lockfile, Bun 1.3's isolated `.bun` store), `vlt` (`.vlt` store), `pip` (hash-pinned `requirements.txt`), `uv`, `pylock` (PEP 751), `poetry`, `pipenv`, `pdm`, `bundler`, `composer`, `cargo`, -`golang`, `nuget` and `maven`. Deno has no hosted rewrite and is not -benchmarked separately. +`golang`, `nuget`, `maven` and `gradle` (`gradle.lockfile`, Gradle's +`modules-2/files-2.1` cache; hosted mode wires the build through +`.socket/gradle/`). Deno has no hosted rewrite and is not benchmarked +separately. Sizes are a large-but-ordinary project for the ecosystem (3000 npm-family packages, 1500 for vlt, 400-1200 for the others, so every scan takes about @@ -75,8 +77,9 @@ unexpected). A rescan's first, preparing scan is untimed. The environment is rebuilt from nothing for every run (`env -i`): `HOME`, `XDG_*` and `TMPDIR` point into the fixture, so per-user caches -(`~/.cargo`, `~/go/pkg/mod`, `~/.nuget/packages`, `~/.m2`) are the -fixture's own and the runner's are never read; telemetry, the update check +(`~/.cargo`, `~/go/pkg/mod`, `~/.nuget/packages`, `~/.m2`, and +`GRADLE_USER_HOME`, which the Gradle fixture sets) are the fixture's own +and the runner's are never read; telemetry, the update check and the persisted Socket login are off; and every proxy variable points at a closed port, so a request to anything but the mock fails the run instead of timing the internet. Python fixtures carry a project `.venv` and Ruby ones diff --git a/crates/socket-patch-bench/src/fixtures/mod.rs b/crates/socket-patch-bench/src/fixtures/mod.rs index 8ea199430..ce9a74eee 100644 --- a/crates/socket-patch-bench/src/fixtures/mod.rs +++ b/crates/socket-patch-bench/src/fixtures/mod.rs @@ -230,6 +230,13 @@ pub static ALL: &[Pm] = &[ patched: 25, build: other::build_maven, }, + Pm { + name: "gradle", + description: "Gradle (build.gradle + gradle.lockfile, ~/.gradle/caches)", + packages: 1000, + patched: 25, + build: other::build_gradle, + }, ]; #[cfg(test)] diff --git a/crates/socket-patch-bench/src/fixtures/other.rs b/crates/socket-patch-bench/src/fixtures/other.rs index 09adc07bb..3b4ffdb42 100644 --- a/crates/socket-patch-bench/src/fixtures/other.rs +++ b/crates/socket-patch-bench/src/fixtures/other.rs @@ -712,9 +712,11 @@ pub fn build_nuget(t: &mut Tree, size: Size) -> std::io::Result { // ── Maven ────────────────────────────────────────────────────────────── -pub fn build_maven(t: &mut Tree, size: Size) -> std::io::Result { - let arts = universe( - "maven", +/// The Maven-coordinate universe the Maven and Gradle fixtures share +/// (`group:artifact` names). +fn jvm_universe(seed: &str, size: Size) -> Vec { + universe( + seed, size, 0.2, |r, i| { @@ -731,11 +733,27 @@ pub fn build_maven(t: &mut Tree, size: Size) -> std::io::Result { format!("{g}:{a}") }, gen::version, - ); - let ga = |p: &Pkg| -> (String, String) { - let (g, a) = p.name.split_once(':').unwrap(); - (g.to_string(), a.to_string()) - }; + ) +} + +fn ga(p: &Pkg) -> (String, String) { + let (g, a) = p.name.split_once(':').unwrap(); + (g.to_string(), a.to_string()) +} + +/// A dependency's pom, listing its own dependencies. +fn jvm_pom(arts: &[Pkg], p: &Pkg) -> String { + let (g, a) = ga(p); + let mut deps = String::new(); + for &j in &p.deps { + let (dg, da) = ga(&arts[j]); + let _ = write!(deps, " \n {dg}\n {da}\n {}\n \n", arts[j].version); + } + format!("\n\n 4.0.0\n {g}\n {a}\n {}\n \n{deps} \n\n", p.version) +} + +pub fn build_maven(t: &mut Tree, size: Size) -> std::io::Result { + let arts = jvm_universe("maven", size); let mut pom = String::from("\n\n 4.0.0\n dev.socket.bench\n bench-app\n 1.0.0\n jar\n\n \n 17\n \n\n \n"); for p in arts.iter().filter(|p| p.direct) { let (g, a) = ga(p); @@ -754,12 +772,7 @@ pub fn build_maven(t: &mut Tree, size: Size) -> std::io::Result { g.replace('.', "/"), p.version ); - let mut deps = String::new(); - for &j in &p.deps { - let (dg, da) = ga(&arts[j]); - let _ = write!(deps, " \n {dg}\n {da}\n {}\n \n", arts[j].version); - } - let pom = format!("\n\n 4.0.0\n {g}\n {a}\n {}\n \n{deps} \n\n", p.version); + let pom = jvm_pom(&arts, p); t.write( &format!("{dir}/{a}-{}.pom.sha1", p.version), gen::sha1_hex(&pom), @@ -778,36 +791,10 @@ pub fn build_maven(t: &mut Tree, size: Size) -> std::io::Result { .iter() .filter(|p| p.patched) .map(|p| { - let (g, a) = ga(p); - let purl = format!("pkg:maven/{g}/{a}@{}", p.version); - let (uuid, token) = grant(&purl); - let suffixed = format!("{}-socket.{}", p.version, &uuid[..8]); - let url = format!("{PATCH_HOST}/patch/maven/{g}/{a}/{}/{token}/{uuid}/{a}-{suffixed}.jar", p.version); - spec( - purl.clone(), - uuid.clone(), - &format!("package/{a}.class"), - json!({ - "status": "granted", - "url": url, - "purl": purl, - "artifacts": [{ "kind": "tarball", "url": url, "integrity": { - "sha256": gen::sha256_hex(&format!("patched-jar:{}", p.name)), - "sha1": gen::sha1_hex(&format!("patched-jar:{}", p.name)), - } }], - "registryOverride": { - "kind": "maven2", - "indexUrl": format!("{PATCH_HOST}/patch-registry/maven/{token}/{uuid}/maven2"), - "identifiers": { - "name": format!("{g}/{a}"), - "version": p.version, - "mavenGroupId": g, - "mavenArtifactId": a, - "mavenSuffixedVersion": suffixed, - "mavenPomSha256": gen::sha256_hex(&format!("patched-pom:{}", p.name)), - }, - }, - }), + jvm_patch( + p, + |token, uuid| format!("{PATCH_HOST}/patch-registry/maven/{token}/{uuid}/maven2"), + false, ) }) .collect(); @@ -822,3 +809,139 @@ pub fn build_maven(t: &mut Tree, size: Size) -> std::io::Result { &[], )) } + +/// The mock's patch for one Maven coordinate: a suffixed-version maven2 +/// registry override. `index_url` builds the override's repository URL +/// from the grant token and patch uuid. +fn jvm_patch(p: &Pkg, index_url: impl Fn(&str, &str) -> String, module_sha: bool) -> PatchSpec { + let (g, a) = ga(p); + let purl = format!("pkg:maven/{g}/{a}@{}", p.version); + let (uuid, token) = grant(&purl); + let suffixed = format!("{}-socket.{}", p.version, &uuid[..8]); + let url = format!( + "{PATCH_HOST}/patch/maven/{g}/{a}/{}/{token}/{uuid}/{a}-{suffixed}.jar", + p.version + ); + let mut identifiers = json!({ + "name": format!("{g}/{a}"), + "version": p.version, + "mavenGroupId": g, + "mavenArtifactId": a, + "mavenSuffixedVersion": suffixed, + "mavenPomSha256": gen::sha256_hex(&format!("patched-pom:{}", p.name)), + }); + if module_sha { + identifiers["mavenModuleSha256"] = + json!(gen::sha256_hex(&format!("patched-module:{}", p.name))); + } + spec( + purl.clone(), + uuid.clone(), + &format!("package/{a}.class"), + json!({ + "status": "granted", + "url": url, + "purl": purl, + "artifacts": [{ "kind": "tarball", "url": url, "integrity": { + "sha256": gen::sha256_hex(&format!("patched-jar:{}", p.name)), + "sha1": gen::sha1_hex(&format!("patched-jar:{}", p.name)), + } }], + "registryOverride": { + "kind": "maven2", + "indexUrl": index_url(&token, &uuid), + "identifiers": identifiers, + }, + }), + ) +} + +// ── Gradle ───────────────────────────────────────────────────────────── + +/// A single-project Groovy-DSL build with dependency locking, its +/// dependencies in Gradle's own cache (`modules-2/files-2.1`, jar and pom +/// in separate sha1 dirs). Hosted mode wires the build through an owned +/// settings script and index under `.socket/gradle/` and pins the +/// suffixed versions in `gradle.lockfile`. +pub fn build_gradle(t: &mut Tree, size: Size) -> std::io::Result { + let arts = jvm_universe("gradle", size); + t.write( + "project/settings.gradle", + "rootProject.name = 'bench-app'\n", + )?; + let mut build = String::from( + "plugins {\n id 'java'\n}\n\nrepositories {\n mavenCentral()\n}\n\ndependencyLocking {\n lockAllConfigurations()\n}\n\ndependencies {\n", + ); + for p in arts.iter().filter(|p| p.direct) { + let _ = writeln!(build, " implementation '{}:{}'", p.name, p.version); + } + build.push_str("}\n"); + t.write("project/build.gradle", build)?; + let mut sorted: Vec<&Pkg> = arts.iter().collect(); + sorted.sort_by(|a, b| a.name.cmp(&b.name)); + let mut lock = String::from( + "# This is a Gradle generated file for dependency locking.\n# Manual edits can break the build and are not advised.\n# This file is expected to be part of source control.\n", + ); + for p in &sorted { + let _ = writeln!( + lock, + "{}:{}=compileClasspath,runtimeClasspath", + p.name, p.version + ); + } + lock.push_str("empty=annotationProcessor,testAnnotationProcessor\n"); + t.write("project/gradle.lockfile", lock)?; + t.write( + "project/gradle/wrapper/gradle-wrapper.properties", + "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.10.2-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n", + )?; + t.write( + "project/src/main/java/App.java", + "public class App { public static void main(String[] a) {} }\n", + )?; + for p in &arts { + let (g, a) = ga(p); + let dir = format!( + "home/.gradle/caches/modules-2/files-2.1/{g}/{a}/{}", + p.version + ); + let pom = jvm_pom(&arts, p); + let jar = format!("PK synthetic {}", p.name); + t.write( + &format!("{dir}/{}/{a}-{}.pom", gen::sha1_hex(&pom), p.version), + pom, + )?; + t.write( + &format!("{dir}/{}/{a}-{}.jar", gen::sha1_hex(&jar), p.version), + jar, + )?; + } + // Gradle's planner only takes https repositories; the CLI never + // fetches the index during a scan, so it need not be the mock. + let patches = arts + .iter() + .filter(|p| p.patched) + .map(|p| { + jvm_patch( + p, + |token, uuid| { + format!("https://patch.socket.dev/patch-registry/maven/{token}/{uuid}/maven2") + }, + true, + ) + }) + .collect(); + let mut f = fixture( + arts.len(), + patches, + &[ + ".socket/gradle/.gitattributes", + ".socket/gradle/hosted-index.tsv", + ".socket/gradle/socket-patch.hosted.settings.gradle", + "gradle.lockfile", + "settings.gradle", + ], + &["redirect_gradle_detached_configs_unguarded"], + ); + f.env_paths = vec![("GRADLE_USER_HOME", "home/.gradle")]; + Ok(f) +} From bb74cac798ee825022b4fcb821c6e5031eabb024 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 18:24:21 +0000 Subject: [PATCH 2/3] Route Gradle digests through utils::digest main has failed socket-patch-core's lib tests since Gradle support (#646) and the digest helpers (#865) both landed. The guard test production_digests_go_through_the_helpers flags three files #646 added that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and patch/sidecars/maven.rs. That breaks test, test-release and coverage on every open PR. Each inline sha1/sha256 call now goes through sha1_hex_of or sha256_hex_of, which compute the same lowercase hex. Behaviour is unchanged. Assisted-by: Claude Code:claude-opus-5-5 (cherry picked from commit 659ac2c24e5c5904e743b4bc98ea4645da2ed6a1) --- crates/socket-patch-core/src/crawlers/gradle_cache.rs | 9 ++++----- crates/socket-patch-core/src/patch/jvm_jar.rs | 7 ++----- crates/socket-patch-core/src/patch/sidecars/maven.rs | 4 +--- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/crates/socket-patch-core/src/crawlers/gradle_cache.rs b/crates/socket-patch-core/src/crawlers/gradle_cache.rs index ef295ee27..afd7c4fba 100644 --- a/crates/socket-patch-core/src/crawlers/gradle_cache.rs +++ b/crates/socket-patch-core/src/crawlers/gradle_cache.rs @@ -70,8 +70,7 @@ pub fn hash_eq(dir_name: &str, sha1_hex: &str) -> bool { /// Whether `bytes` are the pristine download Gradle stored in the hash /// directory `dir_name` (their sha1 names it). pub fn pristine(dir_name: &str, bytes: &[u8]) -> bool { - use sha1::{Digest, Sha1}; - hash_eq(dir_name, &hex::encode(Sha1::digest(bytes))) + hash_eq(dir_name, &crate::utils::digest::sha1_hex_of(bytes)) } /// Whether `path` is a version directory of a `files-2.1` tree @@ -432,8 +431,6 @@ impl DerivedIndex { /// The [`DerivedCopies`] of the jar `jar_leaf` whose pristine bytes /// hash to `pristine_sha1`. pub fn query(&self, jar_leaf: &str, pristine_sha1: &str) -> DerivedCopies { - use sha1::{Digest, Sha1}; - let instrumented = format!("instrumented-{jar_leaf}"); let mut out = DerivedCopies { incomplete: self.incomplete, @@ -460,7 +457,9 @@ impl DerivedIndex { out.stale.push(path.clone()); } else if name == jar_leaf || name == instrumented { match crate::utils::fs::read_regular_to_bytes_sync(path) { - Ok(bytes) if hash_eq(&hex::encode(Sha1::digest(&bytes)), pristine_sha1) => { + Ok(bytes) + if hash_eq(&crate::utils::digest::sha1_hex_of(&bytes), pristine_sha1) => + { out.stale.push(path.clone()) } Ok(_) => out.unknown.push(path.clone()), diff --git a/crates/socket-patch-core/src/patch/jvm_jar.rs b/crates/socket-patch-core/src/patch/jvm_jar.rs index 82d679406..f38a84403 100644 --- a/crates/socket-patch-core/src/patch/jvm_jar.rs +++ b/crates/socket-patch-core/src/patch/jvm_jar.rs @@ -25,8 +25,6 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; -use sha1::Digest as _; - use crate::crawlers::gradle_cache; use crate::hash::git_sha256::compute_git_sha256_from_bytes; use crate::manifest::schema::PatchFileInfo; @@ -353,12 +351,11 @@ fn unpatched_members( } fn sha256_hex(bytes: &[u8]) -> String { - use sha2::Digest as _; - hex::encode(sha2::Sha256::digest(bytes)) + crate::utils::digest::sha256_hex_of(bytes) } fn sha1_hex(bytes: &[u8]) -> String { - hex::encode(sha1::Sha1::digest(bytes)) + crate::utils::digest::sha1_hex_of(bytes) } /// `/jvm-originals/.jar`. diff --git a/crates/socket-patch-core/src/patch/sidecars/maven.rs b/crates/socket-patch-core/src/patch/sidecars/maven.rs index f2f5a2466..8798bfce6 100644 --- a/crates/socket-patch-core/src/patch/sidecars/maven.rs +++ b/crates/socket-patch-core/src/patch/sidecars/maven.rs @@ -17,8 +17,6 @@ use std::path::{Path, PathBuf}; -use sha1::Digest as _; - use super::{ SidecarAdvisory, SidecarAdvisoryCode, SidecarError, SidecarFile, SidecarFileAction, SidecarPayload, SidecarSeverity, @@ -44,7 +42,7 @@ impl Algo { fn digest(self, bytes: &[u8]) -> String { match self { - Algo::Sha1 => hex::encode(sha1::Sha1::digest(bytes)), + Algo::Sha1 => crate::utils::digest::sha1_hex_of(bytes), Algo::Md5 => hex::encode(md5(bytes)), } } From 5b9ae8ea838ec8cd9e6355d23a2986a0bf805c30 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 07:59:48 +0000 Subject: [PATCH 3/3] Bench: add hatch hosted and rescan scenarios Hatch hosted mode (#680, #743) rewrites pyproject.toml and hatch.toml in place, with no lockfile, through utils::hatch::plan. No scenario exercised that rewriter: hatch.toml is a HOSTED pypi input, and a hatch project with no lock fell through every existing pypi fixture. The fixture is a lockless hatchling app. Direct deps go in [project], and a hatch.toml default env (in-project .venv) pins every patched transitive, since hosted Hatch only redirects deps a Hatch table declares. A scan rewrites both files and adds [tool.hatch.metadata] allow-direct-references. It is sized at 1000 packages / 25 patched so a scan takes about 75-85 ms. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/socket-patch-bench/README.md | 3 +- crates/socket-patch-bench/src/fixtures/mod.rs | 7 +++++ .../socket-patch-bench/src/fixtures/pypi.rs | 29 +++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/crates/socket-patch-bench/README.md b/crates/socket-patch-bench/README.md index fe94f6cf9..408e73cbc 100644 --- a/crates/socket-patch-bench/README.md +++ b/crates/socket-patch-bench/README.md @@ -38,7 +38,8 @@ layout: `npm`, `pnpm` (isolated `.pnpm` store with symlinks), `yarn-classic`, `yarn-berry` (node-modules linker), `bun` (text `bun.lock`, hoisted), `bun-isolated` (the same lockfile, Bun 1.3's isolated `.bun` store), `vlt` (`.vlt` store), `pip` (hash-pinned `requirements.txt`), `uv`, `pylock` -(PEP 751), `poetry`, `pipenv`, `pdm`, `bundler`, `composer`, `cargo`, +(PEP 751), `poetry`, `pipenv`, `pdm`, `hatch` (lockless: `hatch.toml` +environment pins, rewritten in place), `bundler`, `composer`, `cargo`, `golang`, `nuget`, `maven` and `gradle` (`gradle.lockfile`, Gradle's `modules-2/files-2.1` cache; hosted mode wires the build through `.socket/gradle/`). Deno has no hosted rewrite and is not benchmarked diff --git a/crates/socket-patch-bench/src/fixtures/mod.rs b/crates/socket-patch-bench/src/fixtures/mod.rs index ce9a74eee..d02863f64 100644 --- a/crates/socket-patch-bench/src/fixtures/mod.rs +++ b/crates/socket-patch-bench/src/fixtures/mod.rs @@ -188,6 +188,13 @@ pub static ALL: &[Pm] = &[ patched: 12, build: pypi::build_pdm, }, + Pm { + name: "hatch", + description: "Hatch (hatchling pyproject + hatch.toml envs, .venv)", + packages: 1000, + patched: 25, + build: pypi::build_hatch, + }, Pm { name: "bundler", description: "RubyGems (Gemfile.lock with CHECKSUMS, vendor/bundle)", diff --git a/crates/socket-patch-bench/src/fixtures/pypi.rs b/crates/socket-patch-bench/src/fixtures/pypi.rs index a785f6c3a..c293fd92a 100644 --- a/crates/socket-patch-bench/src/fixtures/pypi.rs +++ b/crates/socket-patch-bench/src/fixtures/pypi.rs @@ -483,3 +483,32 @@ pub fn build_pdm(t: &mut Tree, size: Size) -> std::io::Result { t.mkdir("home")?; Ok(fixture(&ds, &["pdm.lock"], &[])) } + +// ── hatch ────────────────────────────────────────────────────────────── + +/// A lockless Hatch app: hatchling backend, the direct deps in +/// `[project]`, and a `hatch.toml` default environment (in-project +/// `.venv`) that pins every patched transitive too. Hosted Hatch only +/// redirects deps a Hatch table declares, so the pins are what a real +/// project patching transitive deps writes. +pub fn build_hatch(t: &mut Tree, size: Size) -> std::io::Result { + let ds = dists("hatch", size); + t.write( + "project/pyproject.toml", + pyproject( + &ds, + "\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n", + ), + )?; + let mut s = String::from("[envs.default]\npath = \".venv\"\ndependencies = [\n"); + for (i, d) in ds.iter().enumerate() { + if d.patched || i % 10 == 0 { + let _ = writeln!(s, " \"{}=={}\",", d.name, d.version); + } + } + s.push_str("]\n\n[envs.default.scripts]\ntest = \"python -m unittest\"\n"); + t.write("project/hatch.toml", s)?; + install_venv(t, &ds)?; + t.mkdir("home")?; + Ok(fixture(&ds, &["hatch.toml", "pyproject.toml"], &[])) +}