Resolve each project's imports through its own remappings - #205
Open
shellygr wants to merge 3 commits into
Open
Conversation
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>
jar-ben
approved these changes
Sep 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 pinsthe 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_sourcesnow also reads the other projects under the run root andemits their entries under a context of that project's directory:
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:
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 adependency is not mistaken for a project.
it names a directory inside that project.
_rebase_contextleaves a context as authored when itnames 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.
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.
build_packages_from_remapping_sourcesitself, which is what thecompilation 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
ConflictingPragmaErrornaming both files, both specs and both versions.Joint satisfiability is decided by
pragma_admits, not by comparing resolved versions:>=0.7.0followed by
=0.7.6names two versions and converges on one compiler, so a version comparison wouldcall 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 conflicttest 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>'). Onecontract 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
ConflictingPragmaErrorpropagates out offixconf.fix_conf, which catchesUnsatisfiableSolcPinErrorand downgrades it to a logged error. That matches howUnimplementedContractErroralready behaves there, so it is left as is, but it is new code reachingthat path.
Testing
tests/test_remappings.pygains a section covering the core pair, a sibling whose build failed beingscoped 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.pygains the rebuild test, proving the workaround pathreproduces 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.