Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
934 changes: 920 additions & 14 deletions crates/socket-patch-cli/tests/setup_matrix_gem.rs

Large diffs are not rendered by default.

293 changes: 269 additions & 24 deletions crates/socket-patch-core/src/setup/gem/mod.rs

Large diffs are not rendered by default.

295 changes: 220 additions & 75 deletions crates/socket-patch-core/src/setup/gem/templates/plugins.rb.tmpl

Large diffs are not rendered by default.

284 changes: 217 additions & 67 deletions gem/socket-patch-bundler/plugins.rb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion gem/socket-patch-bundler/socket-patch-bundler.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Published form of the socket-patch Bundler plugin (CLI_CONTRACT property:
# "gem" support matrix, Phase 2). `socket-patch setup` today references the
# in-tree plugin under `.socket/bundler-plugin/` via `git:`; once this gem is
# in-tree plugin under `.socket/bundler-plugin/` via `path:`; once this gem is
# published, a follow-up switches the Gemfile directive to
# `plugin "socket-patch-bundler", "~> <major.minor>"`. The version is kept in
# sync with the workspace by `scripts/version-sync.sh`.
Expand Down
60 changes: 53 additions & 7 deletions gem/socket-patch/lib/socket_patch/launcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,24 @@ def run(argv)
bin = resolve_binary
if Gem.win_platform?
# Windows has no exec() that replaces the process cleanly for console
# apps; spawn + wait and propagate the child's exit status.
exit(system(bin, *argv) ? $?.exitstatus : 1)
# apps; spawn + wait and propagate the child's real exit status (a
# blanket 1 would erase the CLI's meaningful non-zero codes, e.g.
# `setup --check`'s needs-configuration signal).
ok = system(bin, *argv)
raise LauncherError, "could not run #{bin}" if ok.nil?
exit($?.exitstatus || 1)
else
exec([bin, bin], *argv)
end
rescue LauncherError => e
warn("socket-patch: #{e.message}")
exit(1)
rescue StandardError => e
# First-run download/extract failures outside our own error type (DNS
# outages, TLS errors, ...) must exit cleanly, not escape as raw
# backtraces.
warn("socket-patch: #{e.class}: #{e.message}")
exit(1)
end

class LauncherError < StandardError; end
Expand Down Expand Up @@ -74,7 +84,10 @@ def version
return spec.version.to_s
end
Gem::Specification.find_by_name("socket-patch").version.to_s
rescue StandardError
rescue StandardError, Gem::LoadError
# Gem::MissingSpecError (the gem isn't installed at all — running from
# a checkout) is a Gem::LoadError, which is NOT a StandardError; without
# naming it the documented fallback never engaged.
VERSION
end

Expand Down Expand Up @@ -152,9 +165,33 @@ def download_binary(ver, target, ext, dest)
raise LauncherError, "release archive #{archive} did not contain #{exe}"
end

FileUtils.mkdir_p(File.dirname(dest))
FileUtils.cp(extracted, dest)
File.chmod(0o755, dest) unless Gem.win_platform?
install_executable(extracted, dest)
end
end

# Publish the verified binary into the cache atomically: copy to a temp
# file in the destination dir, set the exec bit, then rename over the
# final path — a concurrent first run can only ever see a complete,
# executable binary, never a torn or not-yet-chmodded one.
def install_executable(src, dest)
FileUtils.mkdir_p(File.dirname(dest))
tmp = File.join(File.dirname(dest), ".#{File.basename(dest)}.#{Process.pid}.tmp")
begin
FileUtils.cp(src, tmp)
File.chmod(0o755, tmp) unless Gem.win_platform?
begin
File.rename(tmp, dest)
rescue SystemCallError
# Windows rename cannot replace an existing file: a concurrent
# first run already published the (identical, verified) binary.
raise unless File.exist?(dest)
end
ensure
begin
File.delete(tmp) if File.file?(tmp)
rescue StandardError
# Leftover temp cleanup is best-effort.
end
end
end

Expand Down Expand Up @@ -223,14 +260,23 @@ def verify_sha256!(path, archive, sums)
raise LauncherError, "checksum mismatch for #{archive} (expected #{expected}, got #{actual})"
end

# Quote a value for interpolation into a PowerShell command: single-quoted
# strings are literal except for embedded single quotes, which are escaped
# by doubling them (paths like `it's here` would otherwise break the
# command).
def powershell_quote(value)
"'#{value.gsub("'", "''")}'"
end

def extract(archive_path, ext, dir)
ok =
if ext == "zip"
# bsdtar (the `tar` on modern Windows) extracts zip; fall back to
# PowerShell Expand-Archive.
system("tar", "-xf", archive_path, "-C", dir) ||
system("powershell", "-NoProfile", "-Command",
"Expand-Archive -Force -LiteralPath '#{archive_path}' -DestinationPath '#{dir}'")
"Expand-Archive -Force -LiteralPath #{powershell_quote(archive_path)} " \
"-DestinationPath #{powershell_quote(dir)}")
else
system("tar", "xzf", archive_path, "-C", dir)
end
Expand Down
4 changes: 2 additions & 2 deletions tests/setup_matrix/matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@
},

{
"ecosystem": "gem", "pm": "bundler", "image": "gem", "hook_family": "none",
"baseline_supported": false,
"ecosystem": "gem", "pm": "bundler", "image": "gem", "hook_family": "bundler-plugin",
"baseline_supported": true,
"package": "colorize", "version": "1.1.0", "purl": "pkg:gem/colorize@1.1.0",
"manifest_key": "package/lib/colorize.rb", "apply_ecosystems": "gem"
},
Expand Down
38 changes: 34 additions & 4 deletions tests/setup_matrix/run-case.sh
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,15 @@ marker_blob() { # $1 = marker -> runnable payload on stdout
esac
}

write_manifest() { # $1=purl $2=key $3=afterHash
write_manifest() { # $1=purl $2=key $3=afterHash $4=beforeHash (default: zero)
local before="${4:-$ZEROHASH}"
cat > .socket/manifest.json <<EOF
{
"patches": {
"$1": {
"uuid": "$UUID",
"exportedAt": "2026-01-01T00:00:00Z",
"files": { "$2": { "beforeHash": "$ZEROHASH", "afterHash": "$3" } },
"files": { "$2": { "beforeHash": "$before", "afterHash": "$3" } },
"vulnerabilities": {},
"description": "setup-matrix synthetic patch",
"license": "MIT",
Expand All @@ -168,6 +169,35 @@ write_manifest() { # $1=purl $2=key $3=afterHash
EOF
}

# gem only: the real git-blob sha256 of the target file inside the published
# .gem, probed via `gem fetch` + `gem unpack` (mirrors docker_e2e_gem's
# beforeHash probe). gem apply is hash-gated with no npm-style
# mismatch-warn-and-apply fallback, so the all-zeros placeholder makes the
# variant gate drop the package and the install hook's apply no-ops. Every
# other ecosystem keeps the zero hash — their fixtures are byte-identical to
# before this helper existed.
resolve_before_hash() {
if [ "$SM_ECOSYSTEM" != gem ]; then
printf '%s' "$ZEROHASH"
return
fi
local dir rel target
dir="$(mktemp -d)"
rel="${SM_MANIFEST_KEY#package/}"
target="$dir/${SM_PACKAGE}-${SM_VERSION}/$rel"
if (cd "$dir" \
&& gem fetch "$SM_PACKAGE" -v "$SM_VERSION" >/dev/null 2>&1 \
&& gem unpack "${SM_PACKAGE}-${SM_VERSION}.gem" >/dev/null 2>&1) \
&& [ -f "$target" ]; then
git_sha256 "$target"
else
# The function runs inside $(...): route the log PAST the capture pipe.
log "gem beforeHash probe failed; falling back to the zero placeholder" >&2
printf '%s' "$ZEROHASH"
fi
rm -rf "$dir"
}

build_fixture() {
# Ablation: no patch set committed at all (no .socket/). Even with a
# working install hook, apply finds no manifest and no-ops, so the
Expand Down Expand Up @@ -196,11 +226,11 @@ build_fixture() {
alt)
marker_blob "$SM_ALT_MARKER" > "$blob_tmp"
local h; h="$(git_sha256 "$blob_tmp")"; cp "$blob_tmp" ".socket/blobs/$h"
write_manifest "$SM_PURL" "$SM_MANIFEST_KEY" "$h" ;;
write_manifest "$SM_PURL" "$SM_MANIFEST_KEY" "$h" "$(resolve_before_hash)" ;;
*) # primary
marker_blob "$SM_MARKER" > "$blob_tmp"
local h; h="$(git_sha256 "$blob_tmp")"; cp "$blob_tmp" ".socket/blobs/$h"
write_manifest "$SM_PURL" "$SM_MANIFEST_KEY" "$h" ;;
write_manifest "$SM_PURL" "$SM_MANIFEST_KEY" "$h" "$(resolve_before_hash)" ;;
esac
rm -f "$blob_tmp"
}
Expand Down
Loading