Skip to content

fix(ci): order openzeppelin remappings so both shipyard-core import styles resolve - #28

Merged
ryanio merged 1 commit into
mainfrom
fix/openzeppelin-remapping-order
Aug 22, 2026
Merged

fix(ci): order openzeppelin remappings so both shipyard-core import styles resolve#28
ryanio merged 1 commit into
mainfrom
fix/openzeppelin-remapping-order

Conversation

@ryanio

@ryanio ryanio commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Problem

Forge Tests has been red on main. On a fresh clone with submodules initialized, FOUNDRY_PROFILE=CI forge build fails:

Error (6275): Source "lib/openzeppelin-contracts/contracts/contracts/utils/structs/EnumerableSet.sol" not found
 --> lib/shipyard-core/src/dynamic-traits/DynamicTraits.sol:4:1
 --> lib/shipyard-core/src/dynamic-traits/OnchainTraits.sol:8:1

Note the doubled contracts/contracts/ segment.

Cause

lib/shipyard-core at the pinned commit imports OpenZeppelin two different ways:

  • openzeppelin-contracts/token/ERC721/ERC721.sol
  • openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol

foundry.toml maps both prefixes to the same target to cover that:

'openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/',
'openzeppelin-contracts/contracts/=lib/openzeppelin-contracts/contracts/',

solc resolves overlapping remappings by longest matching prefix, so both styles work when solc is invoked directly. Forge's own import resolver takes the first remapping that matches instead, and it builds the source graph before handing sources to solc. With the bare prefix first, openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol becomes lib/openzeppelin-contracts/contracts/ + contracts/utils/structs/EnumerableSet.sol, which does not exist, so the file never makes it into the compilation unit.

I confirmed the split by running solc 0.8.20 directly with the same pair of remappings in both orders: both compile. Only the Forge resolver is order sensitive.

Fix

List the longer prefix first, with a comment so the pair does not get re-sorted later.

The bare-style imports resolve to exactly the same paths as before, because openzeppelin-contracts/token/... never matched the longer prefix. Nothing moves in the compiled output, the pinned solc stays at 0.8.20, and no submodule pointer changes.

Why not bump the submodules

That was the other option, and I tried it. Upstream shipyard-core did fix this at the source in ProjectOpenSea/shipyard-core#29, but that PR normalizes every OpenZeppelin import to @openzeppelin/contracts/... and bumps OpenZeppelin to v5.5.0 in the same change, so there is no commit that fixes the imports alone. Taking it here pulls in:

  • OpenZeppelin v4.9.2 to v5.5.0, whose sources require ^0.8.24, so this repo's pinned solc = '0.8.20' has to move too. That changes the bytecode of every project generated from the template.
  • 58 commits of shipyard-core drift, including the ERC-7496 rework in Update IERC7496 and DynamicTraits based on ethereum/ERCs PR 1400. src/Dockmaster.sol stops compiling against it: Error (7576): Undeclared identifier on _traits[tokenId] at Dockmaster.sol:244.
  • Remapping changes for solady (solady/src/... rather than solady/...) and seaport-types, which means editing the template's own imports as well.

That is a dependency modernization worth doing on its own, with a migration for Dockmaster.sol and a deliberate choice of compiler pin. Bundling it into a CI fix would ship a bytecode change and a breaking OpenZeppelin major to every generated repo. I would rather unbreak CI first.

Verification

Fresh clone at this branch, git submodule update --init --recursive, then:

  • FOUNDRY_PROFILE=CI forge build - exit 0, compiler run successful
  • FOUNDRY_PROFILE=ci forge build --sizes (the workflow's build step) - exit 0
  • FOUNDRY_PROFILE=CI forge test -vvv - 4 passed, 0 failed
  • forge fmt --check - clean

Forge 1.5.1-stable, solc 0.8.20.

This touches only foundry.toml, so it does not overlap #27.

🤖 Generated with Claude Code

shipyard-core imports OpenZeppelin two ways, "openzeppelin-contracts/token/..."
and "openzeppelin-contracts/contracts/token/...", so foundry.toml maps both
prefixes to the same target. Forge's import resolver takes the first remapping
that matches rather than the longest, so with the bare prefix listed first every
"openzeppelin-contracts/contracts/..." import resolved to a doubled
lib/openzeppelin-contracts/contracts/contracts/... path and the build failed on
EnumerableSet.sol in DynamicTraits.sol and OnchainTraits.sol.

Putting the longer prefix first fixes resolution. Paths for the bare style are
unchanged, so no compiled output moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ryanio
ryanio merged commit bd3808c into main Aug 22, 2026
4 checks passed
@ryanio
ryanio deleted the fix/openzeppelin-remapping-order branch August 22, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant