Skip to content

Vendored dry run gives no symlink warning for uv script locks or pylock files: a symlinked s.py, s.py.lock, pylock.toml or pylock.<name>.toml previews clean, and the real run then refuses (exit 1) #891

Open

Description

[agent] Found by the scheduled uv bug-hunt routine (ledger #310).

Summary

#802 added vendor_would_refuse_symlinked_file, so that vendor --dry-run and the scan / get --mode vendored --dry-run previews predict the wet run's symlink refusal. Its follow-up commits ("Warn about symlinked pom.xml and hatch.toml too") treat "the real run refuses with no dry-run hint" as a bug. The warning list comes from formats::registry::wiring_paths("pypi"), which holds only fixed file names (requirements.txt, uv.lock, pyproject.toml, poetry.lock, …). The PyPI lanes that write files with variable names are left out:

  • the PEP 723 script lane: <script>.py and <script>.py.lock
  • the PEP 751 lane: pylock.toml and pylock.<name>.toml (uv writes these with uv export --format pylock.toml)

When one of those is a symlink, the dry run exits 0 with no warning. The wet run then refuses with pypi_lock_symlink_unsupported (exit 1, partial_failure). The refusal itself is correct and writes nothing. Only the preview is wrong.

Impact

A user or CI step that gates on the dry run sees a clean preview, and the real vendoring then fails. That's exactly the misprediction #802's dry-run warning was added to prevent. It shows up for the uv script and pylock layouts; the uv.lock / pyproject.toml lane is covered.

Repro (real uv, local mock patch server)

mkdir -p shared proj && cd proj && git init -q
cat > s.py <<'EOF'
# /// script
# requires-python = ">=3.9"
# dependencies = ["six==1.16.0", "attrs>=20"]
# ///
import six
EOF
uv lock -q --script s.py
mv s.py.lock ../shared/ && ln -s ../shared/s.py.lock s.py.lock     # or link s.py, pylock.toml, pylock.dev.toml
socket-patch scan --mode vendored --dry-run --json                  # exit 0, no vendor_would_refuse_symlinked_file
socket-patch get pkg:pypi/six@1.16.0 --mode vendored --dry-run --yes --json   # exit 0, no warning
socket-patch scan --mode vendored --yes --json                      # exit 1, partial_failure,
                                                                    # pypi_lock_symlink_unsupported: s.py.lock is a symbolic link …

Control: when uv.lock (or pyproject.toml) is linked instead, both dry runs emit vendor_would_refuse_symlinked_file, and the hosted --dry-run predicts its own refusal (exit 1) for every one of these files.

Expected vs actual

  • Expected: the Fix vendored mode replacing symlinked lockfiles (#627) #802 contract (CLI_CONTRACT and the PR text: "A --dry-run flags each symlinked wiring file with a vendor_would_refuse_symlinked_file advisory"). docs/testing/uv-compatibility.md:132 lists pylock*.toml, *.py.lock and script files among the symlinked files that every writer refuses. The dry run should warn for the same set.
  • Actual: no advisory for script / script-lock / pylock links, while the wet run refuses.

Matrix (main 9c43dfc, Linux)

uv symlinked file scan dry run get dry run wet scan
0.8.17 / 0.12.23 uv.lock (control) warns warns refuses, exit 1
0.8.17 / 0.12.23 s.py no warning, exit 0 no warning refuses, exit 1
0.8.17 / 0.12.23 s.py.lock no warning, exit 0 no warning refuses, exit 1
0.12.23 pylock.toml no warning no warning refuses, exit 1
0.12.23 pylock.dev.toml no warning no warning refuses, exit 1

macOS / Windows probe (0.8.17 / 0.12.23): https://github.com/SocketDev/socket-patch/actions/runs/37370603015 (queued; I'll add the results as a comment). The list is a fixed set of file names, so I expect every OS to behave the same. First bad: #802 (6b8c076) introduced the warning with this gap. Before it, the wet run replaced the link (#627).

Suspect code

  • crates/socket-patch-core/src/formats/registry.rs:246: wiring_paths returns static registry rows only. There are no rows or patterns for *.py.lock, their scripts, or pylock*.toml.
  • crates/socket-patch-cli/src/commands/vendor.rs:401: symlinked_wiring_warnings checks only that list. It could reuse python_lock_paths / script_of_lock (as pypi_lock.rs does for the wet refusal) to cover the files the PyPI lock lane would really write.

Backlog review — 2026-10-08

Priority: P1 → P3. Real writes refuse the symlink safely; only the dry-run preview misses the warning.

Activity

  1. mikolalysenko commented on Oct 5, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] The probe run (https://github.com/SocketDev/socket-patch/actions/runs/37370603015) confirms this on Windows (native symlinks, uv 0.12.23) and on ubuntu-latest (uv 0.12.23). The output is identical on both: uv.lock gets warn=1, while s.py, s.py.lock, pylock.toml and pylock.dev.toml each get warn=0 on the scan and get dry runs, and the wet run fails with pypi_lock_symlink_unsupported (exit 1, link and target untouched). The macOS jobs were still queued when this run ended.


    Generated by Claude Code

  2. added a commit that references this issue on Oct 5, 2026
  3. mikolalysenko commented on Oct 5, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (uv). Confirmed on main: symlinked_wiring_warnings (crates/socket-patch-cli/src/commands/vendor.rs:400) checks only formats::registry::wiring_paths("pypi") (crates/socket-patch-core/src/formats/registry.rs:246), and that list holds only fixed file names. The variable-name PEP 723 (<script>.py, <script>.py.lock) and PEP 751 (pylock.toml, pylock.<name>.toml) wiring files are never probed. A fix needs the dry run to enumerate the variable-name wiring files the wet run would discover. No duplicate or open PR found.


    Generated by Claude Code

  4. mikolalysenko commented on Oct 6, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] macOS evidence from the run-21 probe, which finished after the issue was filed. On main 9c43dfc, macos-latest × uv 0.8.17 and 0.12.23 behave the same as Linux and Windows:

    RESULT 0.12.23 project file=uv.lock         | scan-dry exit=0 warn=1 | get-dry exit=0 warn=1 | wet exit=1 pypi_uv_symlink_unsupported link untouched
    RESULT 0.12.23 script  file=s.py            | scan-dry exit=0 warn=0 | get-dry exit=0 warn=0 | wet exit=1 pypi_lock_symlink_unsupported link untouched
    RESULT 0.12.23 script  file=s.py.lock       | scan-dry exit=0 warn=0 | get-dry exit=0 warn=0 | wet exit=1 pypi_lock_symlink_unsupported link untouched
    RESULT 0.12.23 pylock  file=pylock.toml     | scan-dry exit=0 warn=0 | get-dry exit=0 warn=0 | wet exit=1 pypi_lock_symlink_unsupported link untouched
    RESULT 0.12.23 pylock  file=pylock.dev.toml | scan-dry exit=0 warn=0 | get-dry exit=0 warn=0 | wet exit=1 pypi_lock_symlink_unsupported link untouched
    

    0.8.17 gives the same results (job 111966486095). Job: https://github.com/SocketDev/socket-patch/actions/runs/37370603015/job/111966486055

    That makes the OS table Linux ✗, Windows ✗, macOS ✗.


    Generated by Claude Code

  5. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    v5 triage: P3, not a release blocker. Dry-run parity for symlinked variable-name lockfiles is a narrow refusal diagnostic. The real write already refuses; P3.

    This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.

  6. mikolalysenko commented on Oct 13, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] New data from the uv bug-hunt run on main 15c2c83 (uv 0.13.0, Linux). The symlink case is one symptom of a wider gap: scan / get --mode vendored --dry-run don't run the uv wiring preflight (check_target_guards) at all, but standalone vendor --dry-run does.

    Shape scan/get --mode vendored --dry-run wet scan --mode vendored standalone vendor --dry-run
    [tool.uv] no-sources = true (new #564 refusal, #1434), project lane exit 0, verified exit 1 pypi_uv_no_sources exit 1 failed pypi_uv_no_sources
    same via uv.toml, or no-sources-package = ["six"] exit 0, verified exit 1 pypi_uv_no_sources —
    same three settings, PEP 723 script lane (s.py.lock) exit 0, verified exit 1 pypi_uv_no_sources —
    inline [tool] uv = { sources = … } (#979's shape) exit 0, verified exit 1 pypi_uv_lock_parse_failed (fixed by #980)

    All 6 no-sources cells preview exactly what a clean project previews. The contract lists only the Bun and pnpm-takeover refusals as previewed by the scan dry run, so I'm adding this here instead of opening a new issue. A fix that runs the pypi preflight in the scan/get dry run would cover the symlink, no-sources and inline-sources shapes together.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions