Skip to content

archive output is silently broken when relative imports cross above cwd #5226

Description

@charles-cooper

co-authored by claude fable 5

Version Information

  • vyper Version (output of vyper --version): master (c1b6130 and later)
  • OS: all
  • Python Version: all

What's your issue about?

-f archive can silently produce an archive that fails to recompile, when a source file is reached via a relative import crossing above the cwd, and that file is covered by a search path above the cwd (e.g. -p ..).

Repro:

/tmp/esc/escaped.vy      # x: constant(uint256) = 1
/tmp/esc/pkg/main.vy     # from .. import escaped
                         # y: constant(uint256) = escaped.x

$ cd /tmp/esc/pkg
$ vyper -p .. -f archive -o out.zip main.vy   # succeeds
$ vyper -f bytecode out.zip
vyper.exceptions.ModuleNotFound: escaped

  contract "main.vy:1", line 1:0
  ---> 1 from .. import escaped
  -------^

Mechanism:

  • Archive member names are _anonymize(safe_relpath(resolved_path)) (vyper/compiler/output_bundle.py), which rewrites .. segments to positional digits: ../escaped.vy is stored as 0/escaped.vy. Zip members can't carry .., so some rewrite is unavoidable.
  • Anonymized search paths get the same rewrite (.. → 0), so absolute imports resolved through an above-cwd search path round-trip fine (0/lib/foo.vy is found via search path 0/lib).
  • Relative imports don't use search paths at all — they resolve from the importing module's parent (vyper/semantics/analysis/imports.py, _load_file with level != 0). On recompile, main.vy's parent is the archive root, so the import requests ../escaped.vy, which doesn't exist in the zip.

Note the rewrite is not broken for all above-cwd sources: a relative import whose importer and importee share the same leading .. prefix still round-trips (e.g. ../a.vy and ../b.vy become 0/a.vy and 0/b.vy, and from . import b resolves from parent 0). The failure is an import edge that crosses the anonymized prefix — importer and importee at different .. depths, most commonly inside-cwd importing outside-cwd as above. Any fix should reject/repair only crossing edges, not all above-cwd bundles.

Related: this is the same failure mode that #4706's .-fallback would have produced. When the file is not covered by any search path, used_search_paths errors out at bundle time; the case here bypasses that check because the file is inside a search path.

How can it be fixed?

Options, roughly in order of preference:

  1. Re-root the archive at the common ancestor of all resolved source paths (record the compilation target relative to that root) instead of cwd. All member paths become ..-free, relative imports round-trip naturally, and _anonymize's .. rewriting becomes dead code. Changes member naming for existing above-cwd bundles, so downstream verify tooling should be checked.
  2. Detect and reject at bundle time: during import analysis, flag files loaded via a relative import whose resolution crosses above cwd, and raise a user-facing error (like BundleError) instead of emitting a broken archive. Requires threading import-edge info into the output bundle; used_search_paths alone can't distinguish relative- from absolute-import reachability.
  3. Minimal stopgap: warn when any bundled member path contains rewritten .. segments, noting the archive may not recompile if those files are imported relatively (false positives for absolute-import-only bundles).

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions