foundry icon indicating copy to clipboard operation
foundry copied to clipboard

[wip] feat: compilation restrictions

Open klkvr opened this issue 1 year ago • 1 comments

Closes https://github.com/foundry-rs/foundry/issues/6099 Closes https://github.com/foundry-rs/foundry/issues/5715

This builds on top of https://github.com/foundry-rs/compilers/pull/170 and allows the following configuration options in foundry.toml:

# add via_ir profile
additional_compiler_profiles = { { name = "via-ir", via_ir = true } }

# enforce compiling tests without via_ir and only some heavy contract with via_ir
# note: it will only work if tests are using vm.deployCode to deploy contract
compilation_restrictions = [
    { paths = "test/**", via_ir = false },
    { paths = "src/Counter.sol", via_ir = true },
]

compilation_restrictions allows users to configure how we compile individual files or directories. Currently accepted keys are min_evm_version, max_evm_version, min_optimizer_runs, max_optimizer_runs, cancun. Configured restrictions apply to sources importing the restricted file as well.

Once we have a set of restrictions, we need to somehow construct settings objects to satisfy them. Right now those should be constructed manually via additional_compiler_profiles. It is configured a mapping from profile name to settings overrides. In example above we add a single profile named via-ir, which uses default settings with via_ir enabled, making it possible to compile a single contract under src/ with via-ir while all other contracts, including tests are compiled with default profile.

We need names for all profiles to include them into artifacts names in cases when same source file is compiled with multiple different profiles. Right now such artifacts would be named as Counter.{profile}.json.

When choosing profile, first profile (starting with default one), satisfying restrictions of the source file and all of its imports will be used.

klkvr avatar Aug 14 '24 13:08 klkvr

Not sure if it's related to this or some other unreleased code, but on this branch I've noticed some behaviour where forge test hangs if it needs to build contracts. Then if I run forge build separately it works ok.

bowd avatar Aug 22 '24 13:08 bowd

Thanks for the PR @klkvr. I have a question (related to https://github.com/foundry-rs/foundry/issues/4979):

Does compilation_restrictions also dictate what artifacts will be generated? In your example, will it only generate artifacts for src/Counter.sol and test/** while ignoring the rest?

smol-ninja avatar Oct 12 '24 10:10 smol-ninja

Does compilation_restrictions also dictate what artifacts will be generated? In your example, will it only generate artifacts for src/Counter.sol and test/** while ignoring the rest?

it doesn't. Each project file would still get compiled with either default or custom profile unless you specify --skip flag or config key which dictates which files should be included

klkvr avatar Oct 12 '24 13:10 klkvr

Thanks for the reply. Can you please point me to the config key that can be used to dictate which artifacts would get included?

smol-ninja avatar Oct 12 '24 14:10 smol-ninja

@klkvr Could you please help describe the behaviour when setting min_solc_version | max_solc_version ?

Here's a sample repo: https://github.com/frontier159/forge-compilation-restrictions/blob/main/foundry.toml

Which as it stands uses 0.8.19 for all

Behaviour I would like:

  • All files by default to compile with 0.8.22
    • Might have pragma solidity ^0.8.13; or pragma solidity ^0.8.21;
  • Counter-0.8.19.sol to compile with 0.8.19
    • Since it has a fixed pragma solidity 0.8.19;

It's also not clear to me what additional_compiler_profiles does and what name should be given to it?

frontier159 avatar Dec 02 '24 23:12 frontier159

hey @frontier159

min_solc_version/max_solc_version have no effect, you should use version to specify the solc version restrictions. In your case it would be something like

compilation_restrictions = [
    { paths = "src/Counter-0.8.19.sol", version = ">=0.8.19, <=0.8.22" },
]

additional_compiler_profiles are only needed if you're altering the compiler settings as this feature is not yet smart enough to resolve the additional profiles on its own (like we do with versions)

So for this example if you only need to specify the version restrictions you can just not add any profiles, the version keys in restrictions are treated in the same way as version pragmas in source files.

klkvr avatar Dec 02 '24 23:12 klkvr

hey @frontier159

min_solc_version/max_solc_version have no effect, you should use version to specify the solc version restrictions. In your case it would be something like

compilation_restrictions = [
    { paths = "src/Counter-0.8.19.sol", version = ">=0.8.19, <=0.8.22" },
]

@klkvr In this example repo, if I set to:

[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = '0.8.22'

compilation_restrictions = [
    { paths = "src/Counter-0.8.19.sol", version = ">=0.8.19, <=0.8.22" },
]

Then I get the error:

Error: Encountered invalid solc version in src/Counter-0.8.19.sol: No solc version exists that matches the version requirement: =0.8.19, >=0.8.19, <=0.8.22

Sorry I might need to be spoon fed with the full example

frontier159 avatar Dec 03 '24 00:12 frontier159

you need to remove the solc_version key from the root, it requires all sources to get compiled with the 0.8.22 version thus we can't match the =0.8.19 requirement

klkvr avatar Dec 03 '24 19:12 klkvr

So how do I fully specify that src/Counter-0.8.19.sol should use 0.8.19, and everything else should be 0.8.22?

frontier159 avatar Dec 03 '24 20:12 frontier159

something like this should work

[profile.default]
src = "src"
out = "out"
libs = ["lib"]

compilation_restrictions = [
    { paths = "src/**[!1][!9].sol", version = "=0.8.22" },
    { paths = "src/Counter-0.8.19.sol", version = "=0.8.19" },
]

we don't yet have full regex support for glob patterns, so the first restriction is a bit ugly, you could use a separate directory for this to make it nicer

klkvr avatar Dec 03 '24 20:12 klkvr

OK great thanks @klkvr I found the glob syntax: https://github.com/devongovett/glob-match?tab=readme-ov-file#syntax

It would be nice to still be able to specify the default version for any unmatched paths (eg via solc_version in the default profile)

It will get complicated if we have more than one or two upstream dependencies (eg in libs or node_modules) which need specific versions but then for everything else we want to specify the version.

eg

[profile.default]
src = "src"
out = "out"
libs = ["lib"]

# It would be nice to specify here instead of the last (getting ugly) path below
# solc_version = 0.8.22

compilation_restrictions = [
    { paths = "src/Counter-0.8.19.sol", version = "=0.8.19" },
    { paths = "src/Counter-0.8.20.sol", version = "=0.8.20" },
    { paths = "src/**[!{19,20}].sol", version = "=0.8.22" }
]

Alternatively implement where the order matters, where if a file is matched in an earlier list item it it won't be matched in future ones, so where this could work:

compilation_restrictions = [
    { paths = "src/Counter-0.8.19.sol", version = "=0.8.19" },
    { paths = "src/Counter-0.8.20.sol", version = "=0.8.20" },

    # picks up src/CounterAny.sol, but not src/Counter-0.8.19.sol or src/Counter-0.8.20.sol
    { paths = "**", version = "=0.8.22" } 
]

frontier159 avatar Dec 03 '24 22:12 frontier159

I'm having trouble making this work for https://github.com/ethereum-optimism/optimism/pull/13711

I'm trying to compile FaultDisputeGame.sol at a lower optimizer_runs value, while leaving the rest of the contracts at the high 999999 value.

my settings look like this:

additional_compiler_profiles = [ { name = "test1", optimizer_runs=420 }, {name = "test2", optimizer_runs=999999} ]
compilation_restrictions = [
    { paths = "src/dispute/FaultDisputeGame.sol", optimizer_runs = 420 },
    { paths = "**", optimizer_runs = 999999 }
]

however, i get the following error back:

Screenshot 2025-01-13 at 5 50 55 PM

ostensibly this is because Math.sol is set to compile with 999,999 optimizer_runs. But I want it to compile first with the lower FaultDisputeGame setting, and then later with the higher 999,999 value for all other things that import it. Is this possible? thank you!

wildmolasses avatar Jan 13 '25 22:01 wildmolasses

Since there is no documentation for this, here is a working example:

[profile.default]
src = "contracts/src/main/sol"
test = "test"
libs = ["forge_modules"]
remappings = [
    '@openzeppelin/=node_modules/@openzeppelin/',
    'forge-std=forge_modules/forge-std/src/'
]
via_ir = true
optimizer = true
optimizer_runs = 2000000
bytecode_hash = "none"
cbor_metadata = false

additional_compiler_profiles = [ { name = "low_runs", optimizer_runs=2 }, {name = "default", optimizer_runs=2000000} ]

compilation_restrictions = [
    { paths = "LongContract.sol", optimizer_runs = 2 },
]

I verified the bytecode of LongContract.sol and another contract (with default 2M runs) against output of solc. All other flags (via_ir, etc) are shared correctly.

LongContract.sol lives in contracts/src/main/sol, but the path was unnecessary.

moh-eulith avatar Jan 20 '25 05:01 moh-eulith

something like this should work

[profile.default]
src = "src"
out = "out"
libs = ["lib"]

compilation_restrictions = [
    { paths = "src/**[!1][!9].sol", version = "=0.8.22" },
    { paths = "src/Counter-0.8.19.sol", version = "=0.8.19" },
]

we don't yet have full regex support for glob patterns, so the first restriction is a bit ugly, you could use a separate directory for this to make it nicer

I'm trying this

[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = [
    # Specific remapping for v3-periphery's IERC721Metadata
    "lib/v3-periphery/:@openzeppelin/contracts/token/ERC721/=lib/openzeppelin-contracts/contracts/interfaces/",
    
    # Base remappings
    "@openzeppelin-v4/contracts/=lib/openzeppelin-contracts/contracts/",
    "@openzeppelin-upgradeable/contracts/=lib/openzeppelin-contracts-upgradeable/contracts/",
    "@uniswap/v3-core/=lib/v3-core/",
    "@uniswap/v3-periphery/=lib/v3-periphery/"
]
via_ir = true
optimizer = true
optimizer_runs = 200

compilation_restrictions = [
    {paths="lib/v3-periphery/contracts/libraries/PoolAddress.sol", version = "=0.7.6"},
    { paths = "src/**[!1][!9].sol", version = "=0.8.22" },
    { paths = "test/**[!1][!9].sol", version = "=0.8.22" }
]

because 0.8.22 throws Explicit type conversion not allowed from "uint256" to "address" in that specific file (PoolAddress.sol)

But then I get

Error: Found incompatible versions:
src/core/PrestoFactory.sol ^0.8.17, =0.8.22 imports:
...
    lib/v3-periphery/contracts/interfaces/IPeripheryPayments.sol >=0.7.5
    lib/v3-periphery/contracts/interfaces/IPeripheryImmutableState.sol >=0.5.0
    lib/v3-periphery/contracts/libraries/PoolAddress.sol >=0.5.0, =0.7.6
    lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol ^0.8.0
    lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol ^0.8.0
...

What am i doing wrong?

BrennerSpear avatar Jan 22 '25 16:01 BrennerSpear

@BrennerSpear you can't import lib/v3-periphery/contracts/libraries/PoolAddress.sol which requires concrete =0.7.6 from src/core/PrestoFactory.so requiring concrete =0.8.22

klkvr avatar Jan 22 '25 17:01 klkvr

@BrennerSpear you can't import lib/v3-periphery/contracts/libraries/PoolAddress.sol which requires concrete =0.7.6 from src/core/PrestoFactory.so requiring concrete =0.8.22

So is there no way to use this library with the openzeppelin contracts that require 0.8.22?

BrennerSpear avatar Jan 22 '25 17:01 BrennerSpear

solution for me here was that there is a foundry install [email protected]

BrennerSpear avatar Jan 22 '25 19:01 BrennerSpear