Skip to content

Resolve each project's imports through its own remappings - #205

Open
shellygr wants to merge 3 commits into
masterfrom
shelly/log-mismatch-evidence
Open

Resolve each project's imports through its own remappings#205
shellygr wants to merge 3 commits into
masterfrom
shelly/log-mismatch-evidence

Conversation

@shellygr

@shellygr shellygr commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What

A repository can hold more than one build project. Autosetup read remappings from exactly one of
them, the project that owns the main contract, and applied the result to every file in the run.

A file belonging to a different project then resolved its imports through the anchor's entries. Where
the two projects pin different versions of the same package, the file got the wrong one. Where they
also pin different compilers, the wrong copy carried an incompatible pragma, and one contract's
compilation unit ended up holding two pragmas no single compiler satisfies.

That last case has no repair inside compiler_map, which is keyed by contract. The retry loop pins
the contract from whichever file the compiler reported, gets the other file's pragma on the next
pass, pins it back, and alternates until the conf matches a state it has already tried and stops with
"the conf and command are back to a state that already failed to compile".

Each project's imports resolve through its own remappings

build_packages_from_remapping_sources now also reads the other projects under the run root and
emits their entries under a context of that project's directory:

@pkg/contracts/=<anchor>/node_modules/@pkg/contracts/
<other>/:@pkg/contracts/=<other>/node_modules/@pkg/contracts/

solc matches a context against the importing file's source unit name and prefers the longest match,
so the scoped entry governs that project's files while the anchor's unscoped entries stay the default
for every file no context claims.

Four things this rests on:

  • A project is a directory holding a build config, not one with build artifacts on disk. The
    config declares the project's resolution whether or not it ever compiled, and a build that failed
    for want of the right copy of a package is exactly what these entries answer. The walk prunes
    hidden directories and the vendored-dependency names in DEPENDENCIES, so a config belonging to a
    dependency is not mistaken for a project.
  • A project's own authored context passes through rather than being prefixed again, but only when
    it names a directory inside that project. _rebase_context leaves a context as authored when it
    names nothing, and such a context is longer than the anchor's empty one, so emitting it unchanged
    would let it govern the anchor's files.
  • A scoped entry is only emitted when its target exists. It outranks the global one, so binding a
    prefix to a directory known to be absent would replace a resolution that may work with one that
    cannot. The prefix keeps its existing entry and the collision is logged.
  • The scoping lives in build_packages_from_remapping_sources itself, which is what the
    compilation workarounds call when they rebuild the packages list. Placed anywhere else, the first
    workaround to fire would rebuild the conf without it.

Nothing changes for a repository with one project: discovery finds nothing to scope and the emitted
list is byte-identical, with or without a run root. There is a test for that.

Two pragmas in one unit now stop the run

Where no remapping can reconcile the two files, the loop no longer alternates. Each version pin is
recorded with the file its pragma was read from, and when a contract is pinned again from a different
file whose spec cannot be satisfied together with the new one, the run stops with
ConflictingPragmaError naming both files, both specs and both versions.

Joint satisfiability is decided by pragma_admits, not by comparing resolved versions: >=0.7.0
followed by =0.7.6 names two versions and converges on one compiler, so a version comparison would
call it a conflict. An unparsable spec is not evidence of a conflict and does not stop the run.

The file a pragma was read from is not the file being compiled — solc reports the diagnostic against
the imported file while the Compiling ... line names the importer. Both are recorded; the conflict
test and the message use the former.

The detection says what it read

Detected compiler version mismatch for Foo: requires 0.8.36 (from '^0.8.20' in '<path>'). One
contract can be reported at two versions across a retry loop, and the file and raw spec are what tell
the two readings apart.

For reviewers

ConflictingPragmaError propagates out of fixconf.fix_conf, which catches
UnsatisfiableSolcPinError and downgrades it to a logged error. That matches how
UnimplementedContractError already behaves there, so it is left as is, but it is new code reaching
that path.

Testing

tests/test_remappings.py gains a section covering the core pair, a sibling whose build failed being
scoped anyway, the missing-target policy asserting the scoped key's absence and byte-identity with
the anchor-only list, an authored context not being prefixed twice, an authored context naming
nothing being confined to its project, the context boundary slash against a sibling directory whose
name shares a prefix, single-project invariance, and the discovery boundaries.

tests/test_compilation_workarounds.py gains the rebuild test, proving the workaround path
reproduces the scoped entry, and four conflict cases: incompatible specs from different files stop
the run, converging specs do not, two specs in one file do not, and an unparsable spec does not.

Each new test was checked by reverting the code it covers and confirming it fails.

shellygr and others added 2 commits September 2, 2026 16:06
The detection logged the contract and the resolved version, which is not
enough to tell two detections apart when a retry loop reports one contract
at two different versions. The version is read off a pragma in some file of
that contract's compilation unit, and the contract the error is attributed
to is the one being compiled, not the one the pragma came from, so the file
and the raw spec are what identify the reading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A repository can hold more than one build project, but remappings were read
from only the one owning the main contract and applied to every file in the
run. A file from another project resolved its imports through the anchor's
entries, so where the two pin different versions of a package it got the
wrong copy, and where they also pin different compilers that copy carried a
pragma the file's own compiler cannot parse.

Other projects under the run root now contribute their own entries under a
context of their directory, which solc prefers over the anchor's unscoped
ones for their files. A project is recognised by its build config rather
than by artifacts on disk: the config declares the resolution whether or not
the project ever compiled, and a build that failed for want of the right
copy of a package is what these entries answer.

Where two files in one compilation unit hold pragmas no single compiler
satisfies, compiler_map has no repair to offer, since it is keyed by
contract. That now stops the run with both files, both specs and both
versions rather than pinning the contract from each file in turn until the
conf repeats a state that already failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@shellygr shellygr changed the title Name the file and pragma behind a compiler version mismatch Resolve each project's imports through its own remappings Sep 2, 2026
@shellygr
shellygr requested a review from jar-ben September 2, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants