From db4e8879c94dddf05ebaa803a9fba23c5bfebe5a Mon Sep 17 00:00:00 2001 From: David Meister Date: Mon, 17 Aug 2026 14:50:45 +0000 Subject: [PATCH 1/6] Bump rain-sol-codegen to 0.1.35 and forge-std to 1.16.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `rain-sol-codegen` 0.1.35 replaces the API this repo generated through, so the bump is a rewrite of the calls rather than a version string. forge-std goes with it, forced rather than chosen: 0.1.35's own `LibFs.sol` and `LibCodeGen.sol` import `forge-std-1.16.2/src/Vm.sol`, and a `Vm` from 1.16.1 is a different type at every seam the two libraries share. The licence and the copyright holder are now PARAMETERS of `LibRainDeploySnapshot`, declared in `script/Build.sol` as `GENERATED_SPDX_LICENSE_IDENTIFIER` / `GENERATED_COPYRIGHT_TEXT` and threaded down to `LibCodeGen.filePrefix`. `rain-deploy` is itself published and `rain.factory.deploy` consumes it, so a header chosen inside this library would stamp Rain's licence permanently into another org's append-only `src/generated//` — the defect one layer down that codegen made unrepresentable. `LIB_FS_ROOT` is now `LibFs`'s exported `GENERATED_DIR` rather than a second `"src/generated"` literal held equal to it by an assertion. One constant is what the assertion was standing in for. `snapshotName` is deleted. It existed only to smuggle `dir + "/" + name` through an argument documented to be a Solidity identifier, which codegen now refuses; `LibFs.pathForTaggedContract` takes the directory as a directory. The root-aware `dirForSnapshot`/`pathForSnapshot` overloads carry the writer's own guards in `pathForTaggedContract`'s order, so the reader and the writer agree on which paths EXIST as well as on how they are spelled — `testRootAwareSnapshotPathIsTheWritersAtTheRealRoot` now compares the two as OUTCOMES across the whole fuzz domain, revert data included, rather than only over the names both accept. `writeSnapshot` no longer creates the snapshot directory: the writer creates it from the same tag the path is derived from, so there is no second `createDir` to disagree with it. Fixture directories are renamed to the tag alphabet (`write-snapshot-not-a-tag` -> `writeSnapshotNotATag`, same for the dependencies fixture). They must stay non-`isTag` — a tag-shaped scratch directory under the real `src/generated/` is a release every parallel test suite has to fail on — while being alphabet-legal, because the writer places files only in directories whose names are. Regenerating `src/generated/` and both alias libs with 0.1.35 produces output byte-identical to what is committed: `forge script ./script/Build.sol` followed by `forge fmt` leaves the tree clean. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 8 +- foundry.toml | 4 +- remappings.txt | 4 +- script/Build.sol | 38 ++++- soldeer.lock | 16 +- src/abstract/RainDeployBroadcast.sol | 2 +- src/abstract/RainDeployVerifyBase.sol | 2 +- src/lib/LibRainDeploy.sol | 4 +- src/lib/LibRainDeploySnapshot.sol | 142 ++++++++++------ test/lib/LibMigrationFuzz.sol | 2 +- test/script/Build.t.sol | 2 +- test/src/abstract/RainDeployBroadcast.t.sol | 2 +- test/src/abstract/RainDeploySuitesBase.t.sol | 2 +- test/src/concrete/AddressRegistryGet.t.sol | 2 +- .../concrete/AddressRegistryRegister.t.sol | 2 +- .../concrete/MigrationRegistryApplied.t.sol | 2 +- .../MigrationRegistryApplyMigration.t.sol | 2 +- test/src/concrete/MigrationRegistryHead.t.sol | 2 +- test/src/lib/GeneratedSnapshotShape.t.sol | 2 +- test/src/lib/LibAddressRegistry.t.sol | 2 +- test/src/lib/LibMigrationRegistry.t.sol | 2 +- test/src/lib/LibRainDeploy.t.sol | 2 +- test/src/lib/LibRainDeploySnapshot.t.sol | 157 ++++++++++++++---- 23 files changed, 290 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 498ad76..4ce9f27 100644 --- a/README.md +++ b/README.md @@ -343,8 +343,8 @@ Via [soldeer](https://soldeer.xyz): forge soldeer install rain-deploy~ ``` -**You also need `forge-std` 1.16.1 and `rain-sol-codegen` 0.1.6**, remapped as -`forge-std-1.16.1/` and `rain-sol-codegen-0.1.6/`. The published package ships +**You also need `forge-std` 1.16.2 and `rain-sol-codegen` 0.1.35**, remapped as +`forge-std-1.16.2/` and `rain-sol-codegen-0.1.35/`. The published package ships `src/`, `script/` and the licence and README files — no `test/`, no `foundry.toml`, no `remappings.txt`, no `soldeer.lock`, no `dependencies/` — so a consumer resolves both itself. The requirement is transitive rather than @@ -356,8 +356,8 @@ every abstract a consumer inherits pulls them in — `Script` via ```toml [dependencies] -forge-std = "1.16.1" -rain-sol-codegen = "0.1.6" +forge-std = "1.16.2" +rain-sol-codegen = "0.1.35" rain-deploy = "" ``` diff --git a/foundry.toml b/foundry.toml index 30c1055..c3cb916 100644 --- a/foundry.toml +++ b/foundry.toml @@ -48,8 +48,8 @@ fs_permissions = [ ] [dependencies] -forge-std = "1.16.1" -rain-sol-codegen = "0.1.6" +forge-std = "1.16.2" +rain-sol-codegen = "0.1.35" [soldeer] recursive_deps = false diff --git a/remappings.txt b/remappings.txt index 46ed0ce..24a4a7e 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1,2 @@ -forge-std-1.16.1/=dependencies/forge-std-1.16.1/ -rain-sol-codegen-0.1.6/=dependencies/rain-sol-codegen-0.1.6/ +forge-std-1.16.2/=dependencies/forge-std-1.16.2/ +rain-sol-codegen-0.1.35/=dependencies/rain-sol-codegen-0.1.35/ diff --git a/script/Build.sol b/script/Build.sol index d27f3bc..f437d70 100644 --- a/script/Build.sol +++ b/script/Build.sol @@ -2,11 +2,31 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {Script} from "forge-std-1.16.1/src/Script.sol"; +import {Script} from "forge-std-1.16.2/src/Script.sol"; import {DeployCandidate} from "../src/abstract/RainDeploySuitesBase.sol"; import {RegistryDeploySuites} from "../src/abstract/RegistryDeploySuites.sol"; import {LibRainDeploySnapshot} from "../src/lib/LibRainDeploySnapshot.sol"; +/// @dev The SPDX licence identifier every file this script generates declares. +/// +/// THIS repo's licence, declared by THIS repo, which is the whole point of it +/// being here. `LibRainDeploySnapshot` and `rain-sol-codegen` beneath it take it +/// as a parameter rather than choosing one, because both are dependencies of +/// deploy repos in other orgs, and a header either of them chose would land in +/// those repos' `src/generated//` — append-only, so permanently — under a +/// licence and a copyright holder that are not theirs. +/// +/// `reuse lint` reads the emitted files, so an identifier here with no text in +/// `LICENSES/` fails the legal job. What ties it to the header the committed +/// generated files actually carry is `testWriteAliasLibWritesTheLibAtItsPath`, +/// which emits with these and asserts the result is the committed file byte for +/// byte. +string constant GENERATED_SPDX_LICENSE_IDENTIFIER = "LicenseRef-DCL-1.0"; + +/// @dev The copyright holder every file this script generates names. This repo's, for +/// the reason `GENERATED_SPDX_LICENSE_IDENTIFIER` is. +string constant GENERATED_COPYRIGHT_TEXT = "Copyright (c) 2020 Rain Open Source Software Ltd"; + /// One contract's generated files: the rolling snapshot, the alias lib that /// re-exports its pins and the released-suites lib emitted from its record. /// @@ -134,10 +154,20 @@ contract Build is Script, RegistryDeploySuites { GeneratedContract[] memory contracts = generatedContracts(); for (uint256 i = 0; i < contracts.length; i++) { LibRainDeploySnapshot.writeAliasLib( - vm, contracts[i].contractName, contracts[i].constantPrefix, LibRainDeploySnapshot.CANDIDATE + vm, + contracts[i].contractName, + contracts[i].constantPrefix, + LibRainDeploySnapshot.CANDIDATE, + GENERATED_SPDX_LICENSE_IDENTIFIER, + GENERATED_COPYRIGHT_TEXT ); LibRainDeploySnapshot.writeReleasedSuitesLib( - vm, LibRainDeploySnapshot.LIB_FS_ROOT, contracts[i].contractName, contracts[i].candidate.snapshot + vm, + LibRainDeploySnapshot.LIB_FS_ROOT, + contracts[i].contractName, + GENERATED_SPDX_LICENSE_IDENTIFIER, + GENERATED_COPYRIGHT_TEXT, + contracts[i].candidate.snapshot ); } } @@ -151,6 +181,8 @@ contract Build is Script, RegistryDeploySuites { vm, LibRainDeploySnapshot.CANDIDATE, contracts[i].contractName, + GENERATED_SPDX_LICENSE_IDENTIFIER, + GENERATED_COPYRIGHT_TEXT, contracts[i].candidate.sourceCreationCode, contracts[i].candidate.snapshot.dependencies ); diff --git a/soldeer.lock b/soldeer.lock index 7cfb9a7..5129737 100644 --- a/soldeer.lock +++ b/soldeer.lock @@ -1,13 +1,13 @@ [[dependencies]] name = "forge-std" -version = "1.16.1" -url = "https://soldeer-revisions.s3.amazonaws.com/forge-std/1_16_1_08-05-2026_08:51:16_forge-std-1.16.zip" -checksum = "839b61832925c7152c7b6dffbfa4998d9e606211179bd8f604733124e8a7cb57" -integrity = "60e55d10150354ca4a1e2985c5456c834b92b82ef85ab0e1d92a7786cddbd219" +version = "1.16.2" +url = "https://soldeer-revisions.s3.amazonaws.com/forge-std/1_16_2_03-07-2026_07:37:45_forge-std-1.16.zip" +checksum = "405dccc9d60d753f6abc412b4adf0359f4390f65dbadbbb277a12b4946ed8969" +integrity = "5fb4325b60d7d4194bd0481e2f94398cf1d9c6561dd12a66fb2748ef4a68205f" [[dependencies]] name = "rain-sol-codegen" -version = "0.1.6" -url = "https://soldeer-revisions.s3.amazonaws.com/rain-sol-codegen/0_1_6_13-08-2026_19:05:33_rain.sol.zip" -checksum = "906ebec1dff49612802ce04db626827aba348f4efd174fc7cb483f7d9ce8a06d" -integrity = "2e911fdf161eed28e1d94c50a1cc2ee0d353aa4c9d1a4dc2bdbfa0ac4f3b1de7" +version = "0.1.35" +url = "https://soldeer-revisions.s3.amazonaws.com/rain-sol-codegen/0_1_35_17-08-2026_12:55:14_rain.sol.zip" +checksum = "b4f04fc3ce97461fda3702dfbc12a59cdd06d09e146fb5c81435d91f92a599ab" +integrity = "690dfad8d4f658fbdaf1a5ab94971bdbb34cba500d3d6f94b42088730b251f4c" diff --git a/src/abstract/RainDeployBroadcast.sol b/src/abstract/RainDeployBroadcast.sol index 87b382f..1c4c71c 100644 --- a/src/abstract/RainDeployBroadcast.sol +++ b/src/abstract/RainDeployBroadcast.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity ^0.8.25; -import {Script} from "forge-std-1.16.1/src/Script.sol"; +import {Script} from "forge-std-1.16.2/src/Script.sol"; import {DeploySuite, RainDeploySuitesBase} from "./RainDeploySuitesBase.sol"; import {LibRainDeploy} from "../lib/LibRainDeploy.sol"; diff --git a/src/abstract/RainDeployVerifyBase.sol b/src/abstract/RainDeployVerifyBase.sol index dc62f0d..81b57b4 100644 --- a/src/abstract/RainDeployVerifyBase.sol +++ b/src/abstract/RainDeployVerifyBase.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity ^0.8.25; -import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {Test} from "forge-std-1.16.2/src/Test.sol"; import {DeploySuite, RainDeploySuitesBase} from "./RainDeploySuitesBase.sol"; import {LibRainDeploy} from "../lib/LibRainDeploy.sol"; diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index 8ff7c41..e4d2686 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -2,8 +2,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity ^0.8.25; -import {Vm} from "forge-std-1.16.1/src/Vm.sol"; -import {console2} from "forge-std-1.16.1/src/console2.sol"; +import {Vm} from "forge-std-1.16.2/src/Vm.sol"; +import {console2} from "forge-std-1.16.2/src/console2.sol"; /// @title LibRainDeploy /// Library for deploying contracts via the Zoltu factory across all the networks diff --git a/src/lib/LibRainDeploySnapshot.sol b/src/lib/LibRainDeploySnapshot.sol index a1a6148..736b1da 100644 --- a/src/lib/LibRainDeploySnapshot.sol +++ b/src/lib/LibRainDeploySnapshot.sol @@ -2,9 +2,9 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity ^0.8.25; -import {Vm} from "forge-std-1.16.1/src/Vm.sol"; -import {LibCodeGen} from "rain-sol-codegen-0.1.6/src/lib/LibCodeGen.sol"; -import {LibFs} from "rain-sol-codegen-0.1.6/src/lib/LibFs.sol"; +import {Vm} from "forge-std-1.16.2/src/Vm.sol"; +import {LibCodeGen} from "rain-sol-codegen-0.1.35/src/lib/LibCodeGen.sol"; +import {GENERATED_DIR, LibFs} from "rain-sol-codegen-0.1.35/src/lib/LibFs.sol"; import {DeploySuite} from "../abstract/RainDeploySuitesBase.sol"; import {LibRainDeploy} from "./LibRainDeploy.sol"; @@ -180,41 +180,45 @@ library LibRainDeploySnapshot { return string(tagBytes); } - /// The output root `LibFs` writes to, and the only one it can write to: - /// `LibFs.pathForContract` hardcodes it. + /// The output root `LibFs` writes to, and the only one it can write to. /// - /// The only spelling of that root in this library. Everything here that - /// names the root — the directory a snapshot is written into, and the tree - /// the frozen record is walked from — reads it, so a root this library - /// walks that is not a root it writes to is not a state it can be in. The - /// remaining pair, this and `LibFs`'s own, is what - /// `testRecordRootIsTheRootTheWriterWritesTo` pins. - string constant LIB_FS_ROOT = "src/generated"; + /// `LibFs`'s own constant rather than a copy of its text. Everything here + /// that names the root — the directory a snapshot is written into, and the + /// tree the frozen record is walked from — reads it, so a root this library + /// walks that is not a root it writes to is not a state it can be in. It + /// was a second constant held equal to `LibFs`'s by an assertion until + /// `rain-sol-codegen` exported its own; one constant is the thing an + /// assertion was standing in for. + string constant LIB_FS_ROOT = GENERATED_DIR; /// The directory holding a snapshot, rolling or frozen, under a record /// root. + /// + /// Refuses a directory name `LibFs` would refuse to write into, through + /// `LibFs.requireTag` rather than through a rule restated here. A reader + /// that admitted a name the writer refuses is a reader pointed at a path + /// nothing can ever have written, and a fixture record that admitted one + /// would be a fixture of a layout the real record cannot hold. /// @param root The record root — `LIB_FS_ROOT` for a repo's real record. /// @param dir The snapshot directory name — a release tag, or `CANDIDATE`. + /// MUST be drawn from `LibFs`'s tag alphabet. /// @return The directory path. function dirForSnapshot(string memory root, string memory dir) internal pure returns (string memory) { + LibFs.requireTag(dir); return string.concat(root, "/", dir); } /// The directory holding a snapshot in a repo's REAL record. + /// + /// `LibFs`'s own spelling of it, so the directory this library names is the + /// directory the writer creates and writes into. The root-aware spelling + /// above is the only other one there is, and + /// `testRecordRootIsTheRootTheWriterWritesTo` is where the two are held to + /// being one path. /// @param dir The snapshot directory name — a release tag, or `CANDIDATE`. /// @return The directory path. function dirForSnapshot(string memory dir) internal pure returns (string memory) { - return dirForSnapshot(LIB_FS_ROOT, dir); - } - - /// The contract name that places a generated file inside a snapshot - /// directory. `LibFs` derives its own path from a contract name and takes - /// no path, so the snapshot directory is folded into the name it is given. - /// @param dir The snapshot directory name — a release tag, or `CANDIDATE`. - /// @param contractName The name of the contract. - /// @return The name to pass to `LibFs.buildFileForContract`. - function snapshotName(string memory dir, string memory contractName) internal pure returns (string memory) { - return string.concat(dir, "/", contractName); + return LibFs.dirForTag(dir); } /// The path of a contract's generated file within a snapshot, under a @@ -222,20 +226,30 @@ library LibRainDeploySnapshot { /// /// `LibFs` writes under `LIB_FS_ROOT` and takes no root, so it cannot spell /// this one — but the two MUST be one path where the root is the real one, - /// and `testSnapshotPathsAgreeWithTheWriter` is where that is held. This is - /// the only other spelling of a snapshot path there is, so a reader - /// pointed at a record root and a writer pointed at the real one cannot - /// drift by more than that one assertion. + /// and `testRootAwareSnapshotPathIsTheWritersAtTheRealRoot` is where that is + /// held. This is the only other spelling of a snapshot path there is, so a + /// reader pointed at a record root and a writer pointed at the real one + /// cannot drift by more than that one assertion. + /// + /// Both guards are `LibFs.pathForTaggedContract`'s, asked in its order — + /// the directory first, so a call that gets both wrong names the directory. + /// That is what makes the two spellings agree on which paths EXIST as well + /// as on how they are spelled: a reader that accepted what the writer + /// refuses is the same divergence one step quieter. /// @param root The record root — `LIB_FS_ROOT` for a repo's real record. /// @param dir The snapshot directory name — a release tag, or `CANDIDATE`. - /// @param contractName The name of the contract. + /// MUST be drawn from `LibFs`'s tag alphabet. + /// @param contractName The name of the contract. MUST be a Solidity + /// identifier. /// @return The file path. function pathForSnapshot(string memory root, string memory dir, string memory contractName) internal pure returns (string memory) { - return string.concat(dirForSnapshot(root, dir), "/", contractName, ".sol"); + string memory snapshotDir = dirForSnapshot(root, dir); + LibCodeGen.requireIdentifier(contractName); + return string.concat(snapshotDir, "/", contractName, ".sol"); } /// The path of a contract's generated file within a snapshot in a repo's @@ -244,11 +258,17 @@ library LibRainDeploySnapshot { /// Delegated to `LibFs` rather than concatenated here, so the path this /// library freezes FROM is the same definition `LibFs` writes TO. Two /// spellings of one path is how a freeze silently reads nothing. + /// + /// `pathForTaggedContract` is the entry point that exists for this layout: + /// the snapshot directory is an ARGUMENT, so it is checked as a directory + /// name. It replaced folding the directory into the contract name, which + /// smuggled a path separator through an argument documented to be a + /// Solidity identifier and is refused outright now. /// @param dir The snapshot directory name — a release tag, or `CANDIDATE`. /// @param contractName The name of the contract. /// @return The file path. function pathForSnapshot(string memory dir, string memory contractName) internal pure returns (string memory) { - return LibFs.pathForContract(snapshotName(dir, contractName)); + return LibFs.pathForTaggedContract(dir, contractName); } /// Every file in the FROZEN record: everything inside a release-tag @@ -309,7 +329,7 @@ library LibRainDeploySnapshot { } /// The constants a snapshot declares below the `BYTECODE_HASH` that - /// `LibFs.buildFileForContract` writes itself: the deploy address, the + /// `LibFs.buildFileForTaggedContract` writes itself: the deploy address, the /// creation code, the runtime code and the frozen dependency list, in that /// order. /// @@ -356,12 +376,12 @@ library LibRainDeploySnapshot { /// Generate one snapshot for one contract. /// - /// There is no output root to choose. `LibFs.pathForContract` hardcodes - /// `LIB_FS_ROOT` and takes a contract name rather than a path, and this is - /// the repo's real deploy record, which belongs under that root and nowhere - /// else. This is the one place a snapshot's bytes come into existence, and - /// they come from the compiler rather than from another tree, so there is - /// nothing for a root to select between. + /// There is no output root to choose. `LibFs.buildFileForTaggedContract` + /// derives its directory from `LIB_FS_ROOT` and the snapshot directory it is + /// handed, and this is the repo's real deploy record, which belongs under + /// that root and nowhere else. This is the one place a snapshot's bytes come + /// into existence, and they come from the compiler rather than from another + /// tree, so there is nothing for a root to select between. /// /// `freeze` does take a root and that is not the same freedom: it COPIES, /// within one record tree, reading a rolling snapshot under the root it is @@ -384,9 +404,18 @@ library LibRainDeploySnapshot { /// `abi.encode`d because Solidity has no file-scope constant of dynamic /// array type. The consumer is `releasedLibraryBlock`, which emits the /// matching `abi.decode`. + /// The licence and the copyright holder are the CALLING repo's, so they are + /// parameters here exactly as they are parameters of `LibCodeGen.filePrefix` + /// beneath. A snapshot is a file in the consuming repo's own tree, and this + /// library is a dependency of deploy repos that are not this one — a header + /// decided here would stamp this repo's licence into theirs, permanently, + /// into the one part of their tree that is append-only. /// @param vm The Vm instance for file operations. /// @param dir The snapshot directory name — a release tag, or `CANDIDATE`. /// @param contractName The contract the snapshot describes. + /// @param spdxLicenseIdentifier The SPDX licence identifier the written + /// snapshot declares. + /// @param copyrightText The copyright text the written snapshot declares. /// @param creationCode That contract's creation code. /// @param dependencies The addresses that must already have code on a /// network before this contract can be broadcast there. @@ -395,17 +424,20 @@ library LibRainDeploySnapshot { Vm vm, string memory dir, string memory contractName, + string memory spdxLicenseIdentifier, + string memory copyrightText, bytes memory creationCode, address[] memory dependencies ) internal returns (string memory) { LibRainDeploy.etchZoltuFactory(vm); - //forge-lint: disable-next-line(unsafe-cheatcode) - vm.createDir(dirForSnapshot(dir), true); address deployed = LibRainDeploy.deployZoltu(creationCode); + string memory constants = snapshotConstants(vm, deployed, creationCode, dependencies); - LibFs.buildFileForContract( - vm, deployed, snapshotName(dir, contractName), snapshotConstants(vm, deployed, creationCode, dependencies) + // The directory is created by the writer, from the same tag this path is + // derived from, so there is no `createDir` here to disagree with it. + LibFs.buildFileForTaggedContract( + vm, deployed, dir, contractName, spdxLicenseIdentifier, copyrightText, constants ); return pathForSnapshot(dir, contractName); @@ -483,17 +515,26 @@ library LibRainDeploySnapshot { /// /// The header comes from `LibCodeGen.filePrefix`, the same one `LibFs` /// gives a snapshot, so an alias lib and a snapshot say they are generated - /// in identical words and neither restates the other. + /// in identical words and neither restates the other. The licence and the + /// copyright holder are the calling repo's, for the reason `writeSnapshot` + /// gives, and reach `filePrefix` from here unchanged. /// @param vm The Vm instance for file operations. /// @param contractName The contract the snapshot describes. /// @param constantPrefix The prefix for the emitted constants, e.g. /// `ADDRESS_REGISTRY`. /// @param dir The snapshot directory to alias — ordinarily `CANDIDATE`. + /// @param spdxLicenseIdentifier The SPDX licence identifier the written lib + /// declares. + /// @param copyrightText The copyright text the written lib declares. /// @return The path written. - function writeAliasLib(Vm vm, string memory contractName, string memory constantPrefix, string memory dir) - internal - returns (string memory) - { + function writeAliasLib( + Vm vm, + string memory contractName, + string memory constantPrefix, + string memory dir, + string memory spdxLicenseIdentifier, + string memory copyrightText + ) internal returns (string memory) { string memory libraryName = string.concat("Lib", contractName, "Deploy"); string memory path = string.concat("src/lib/", libraryName, ".sol"); @@ -501,7 +542,7 @@ library LibRainDeploySnapshot { vm.writeFile( path, string.concat( - LibCodeGen.filePrefix(), + LibCodeGen.filePrefix(spdxLicenseIdentifier, copyrightText), "\n", aliasImportBlock(contractName, constantPrefix, dir), aliasLibraryBlock(contractName, constantPrefix, libraryName) @@ -841,12 +882,17 @@ library LibRainDeploySnapshot { /// the real record can only be tested against it, and a repo that has cut /// no release has nothing there to test against. /// @param contractName The contract the released record describes. + /// @param spdxLicenseIdentifier The SPDX licence identifier the written lib + /// declares. The calling repo's, for the reason `writeSnapshot` gives. + /// @param copyrightText The copyright text the written lib declares. /// @param template The candidate declaration the metadata comes from. /// @return The path written. function writeReleasedSuitesLib( Vm vm, string memory recordRoot, string memory contractName, + string memory spdxLicenseIdentifier, + string memory copyrightText, DeploySuite memory template ) internal returns (string memory) { string memory libraryName = string.concat("Lib", contractName, "Released"); @@ -857,7 +903,7 @@ library LibRainDeploySnapshot { vm.writeFile( path, string.concat( - LibCodeGen.filePrefix(), + LibCodeGen.filePrefix(spdxLicenseIdentifier, copyrightText), "\n", releasedImportBlock(vm, paths), releasedLibraryBlock(vm, libraryName, contractName, paths, template) diff --git a/test/lib/LibMigrationFuzz.sol b/test/lib/LibMigrationFuzz.sol index d2f785b..0f394a2 100644 --- a/test/lib/LibMigrationFuzz.sol +++ b/test/lib/LibMigrationFuzz.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity ^0.8.25; -import {Vm} from "forge-std-1.16.1/src/Vm.sol"; +import {Vm} from "forge-std-1.16.2/src/Vm.sol"; import {MIGRATION_HEAD_GENESIS} from "../../src/interface/IMigrationRegistryV1.sol"; diff --git a/test/script/Build.t.sol b/test/script/Build.t.sol index 78423b1..b0a2072 100644 --- a/test/script/Build.t.sol +++ b/test/script/Build.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {Test} from "forge-std-1.16.2/src/Test.sol"; import {GeneratedContract} from "../../script/Build.sol"; import {DeployCandidate} from "../../src/abstract/RainDeploySuitesBase.sol"; import {BuildHarness} from "../concrete/BuildHarness.sol"; diff --git a/test/src/abstract/RainDeployBroadcast.t.sol b/test/src/abstract/RainDeployBroadcast.t.sol index 8923d3c..eca9c39 100644 --- a/test/src/abstract/RainDeployBroadcast.t.sol +++ b/test/src/abstract/RainDeployBroadcast.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {Test} from "forge-std-1.16.2/src/Test.sol"; import {CandidateSourceMismatch, UnknownDeploymentSuite} from "../../../src/abstract/RainDeploySuitesBase.sol"; import {LibRainDeploy} from "../../../src/lib/LibRainDeploy.sol"; diff --git a/test/src/abstract/RainDeploySuitesBase.t.sol b/test/src/abstract/RainDeploySuitesBase.t.sol index bd8e488..dcc877b 100644 --- a/test/src/abstract/RainDeploySuitesBase.t.sol +++ b/test/src/abstract/RainDeploySuitesBase.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {Test} from "forge-std-1.16.2/src/Test.sol"; import { DeploySuite, diff --git a/test/src/concrete/AddressRegistryGet.t.sol b/test/src/concrete/AddressRegistryGet.t.sol index 8198e56..cd21a7b 100644 --- a/test/src/concrete/AddressRegistryGet.t.sol +++ b/test/src/concrete/AddressRegistryGet.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {Test} from "forge-std-1.16.2/src/Test.sol"; import {IAddressRegistryV1} from "../../../src/interface/IAddressRegistryV1.sol"; import {AddressRegistry, ADDRESS_REGISTRY_ROOT} from "../../../src/concrete/AddressRegistry.sol"; diff --git a/test/src/concrete/AddressRegistryRegister.t.sol b/test/src/concrete/AddressRegistryRegister.t.sol index d5d52d9..a2a063e 100644 --- a/test/src/concrete/AddressRegistryRegister.t.sol +++ b/test/src/concrete/AddressRegistryRegister.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {Test, Vm} from "forge-std-1.16.1/src/Test.sol"; +import {Test, Vm} from "forge-std-1.16.2/src/Test.sol"; import {IAddressRegistryV1} from "../../../src/interface/IAddressRegistryV1.sol"; import {AddressRegistry, ADDRESS_REGISTRY_ROOT} from "../../../src/concrete/AddressRegistry.sol"; diff --git a/test/src/concrete/MigrationRegistryApplied.t.sol b/test/src/concrete/MigrationRegistryApplied.t.sol index 307385d..2e1771b 100644 --- a/test/src/concrete/MigrationRegistryApplied.t.sol +++ b/test/src/concrete/MigrationRegistryApplied.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {Test} from "forge-std-1.16.2/src/Test.sol"; import {IMigrationRegistryV1, MIGRATION_HEAD_GENESIS} from "../../../src/interface/IMigrationRegistryV1.sol"; import {MigrationRegistry} from "../../../src/concrete/MigrationRegistry.sol"; diff --git a/test/src/concrete/MigrationRegistryApplyMigration.t.sol b/test/src/concrete/MigrationRegistryApplyMigration.t.sol index c3c3139..d6ce0ac 100644 --- a/test/src/concrete/MigrationRegistryApplyMigration.t.sol +++ b/test/src/concrete/MigrationRegistryApplyMigration.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {Test, Vm} from "forge-std-1.16.1/src/Test.sol"; +import {Test, Vm} from "forge-std-1.16.2/src/Test.sol"; import {IMigrationRegistryV1, MIGRATION_HEAD_GENESIS} from "../../../src/interface/IMigrationRegistryV1.sol"; import {MigrationRegistry} from "../../../src/concrete/MigrationRegistry.sol"; diff --git a/test/src/concrete/MigrationRegistryHead.t.sol b/test/src/concrete/MigrationRegistryHead.t.sol index 1dd0b0b..4702534 100644 --- a/test/src/concrete/MigrationRegistryHead.t.sol +++ b/test/src/concrete/MigrationRegistryHead.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {Test} from "forge-std-1.16.2/src/Test.sol"; import {IMigrationRegistryV1, MIGRATION_HEAD_GENESIS} from "../../../src/interface/IMigrationRegistryV1.sol"; import {MigrationRegistry} from "../../../src/concrete/MigrationRegistry.sol"; diff --git a/test/src/lib/GeneratedSnapshotShape.t.sol b/test/src/lib/GeneratedSnapshotShape.t.sol index 0b34141..ebf0b85 100644 --- a/test/src/lib/GeneratedSnapshotShape.t.sol +++ b/test/src/lib/GeneratedSnapshotShape.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {Test, Vm} from "forge-std-1.16.1/src/Test.sol"; +import {Test, Vm} from "forge-std-1.16.2/src/Test.sol"; import {DeployCandidate} from "../../../src/abstract/RainDeploySuitesBase.sol"; import {RegistryDeploySuites} from "../../../src/abstract/RegistryDeploySuites.sol"; import {LibRainDeploySnapshot} from "../../../src/lib/LibRainDeploySnapshot.sol"; diff --git a/test/src/lib/LibAddressRegistry.t.sol b/test/src/lib/LibAddressRegistry.t.sol index d365018..5070987 100644 --- a/test/src/lib/LibAddressRegistry.t.sol +++ b/test/src/lib/LibAddressRegistry.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {Test} from "forge-std-1.16.2/src/Test.sol"; import {LibAddressRegistry} from "../../../src/lib/LibAddressRegistry.sol"; import {LibAddressRegistryDeploy} from "../../../src/lib/LibAddressRegistryDeploy.sol"; import {LibRainDeploy} from "../../../src/lib/LibRainDeploy.sol"; diff --git a/test/src/lib/LibMigrationRegistry.t.sol b/test/src/lib/LibMigrationRegistry.t.sol index 00ecb95..41bb030 100644 --- a/test/src/lib/LibMigrationRegistry.t.sol +++ b/test/src/lib/LibMigrationRegistry.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {Test} from "forge-std-1.16.2/src/Test.sol"; import {LibMigrationRegistry} from "../../../src/lib/LibMigrationRegistry.sol"; import {LibMigrationRegistryDeploy} from "../../../src/lib/LibMigrationRegistryDeploy.sol"; import {LibRainDeploy} from "../../../src/lib/LibRainDeploy.sol"; diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index e5ad116..1a3c258 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {Test} from "forge-std-1.16.2/src/Test.sol"; import {LibRainDeploy} from "../../../src/lib/LibRainDeploy.sol"; import {IAddressRegistryV1} from "../../../src/interface/IAddressRegistryV1.sol"; import {AddressRegistry, ADDRESS_REGISTRY_ROOT} from "../../../src/concrete/AddressRegistry.sol"; diff --git a/test/src/lib/LibRainDeploySnapshot.t.sol b/test/src/lib/LibRainDeploySnapshot.t.sol index 393dabb..3348f77 100644 --- a/test/src/lib/LibRainDeploySnapshot.t.sol +++ b/test/src/lib/LibRainDeploySnapshot.t.sol @@ -2,8 +2,9 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {Test} from "forge-std-1.16.2/src/Test.sol"; +import {GENERATED_COPYRIGHT_TEXT, GENERATED_SPDX_LICENSE_IDENTIFIER} from "../../../script/Build.sol"; import {DeploySuite} from "../../../src/abstract/RainDeploySuitesBase.sol"; import { EmptyRelease, @@ -60,6 +61,34 @@ contract LibRainDeploySnapshotTest is Test { LibRainDeploySnapshot.checkReleaseFollowsRecord(vm, recordRoot, tag); } + /// External wrapper for the ROOT-AWARE snapshot path, so a refusal is a + /// failed call rather than a reverted test and the two spellings can be + /// compared over the whole fuzz domain. + /// @param root The record root. + /// @param dir The snapshot directory name. + /// @param contractName The name of the contract. + /// @return The file path. + function externalPathForSnapshotAt(string memory root, string memory dir, string memory contractName) + external + pure + returns (string memory) + { + return LibRainDeploySnapshot.pathForSnapshot(root, dir, contractName); + } + + /// External wrapper for the WRITER's snapshot path, the counterpart to + /// `externalPathForSnapshotAt`. + /// @param dir The snapshot directory name. + /// @param contractName The name of the contract. + /// @return The file path. + function externalPathForSnapshot(string memory dir, string memory contractName) + external + pure + returns (string memory) + { + return LibRainDeploySnapshot.pathForSnapshot(dir, contractName); + } + /// A release tag from its components, as `tagForVersion` spells one. /// @param major The major component. /// @param minor The minor component. @@ -249,7 +278,6 @@ contract LibRainDeploySnapshotTest is Test { /// Snapshot paths MUST agree with `LibFs`, which is what writes them. function testSnapshotPathsAgreeWithTheWriter() external pure { assertEq(LibRainDeploySnapshot.dirForSnapshot("0_1_7"), "src/generated/0_1_7"); - assertEq(LibRainDeploySnapshot.snapshotName("0_1_7", "Foo"), "0_1_7/Foo"); assertEq(LibRainDeploySnapshot.pathForSnapshot("0_1_7", "Foo"), "src/generated/0_1_7/Foo.sol"); // The same two paths under a record root that is not the real one, so @@ -275,29 +303,50 @@ contract LibRainDeploySnapshotTest is Test { /// property is about every path either could produce, not about a chosen /// one: a divergence that only appears for some names is exactly the /// silence this is here to remove. + /// + /// Both spellings now REFUSE names they cannot place, so agreeing on the + /// bytes is only half of being one path — one that accepted what the other + /// refuses would read a fixture record at a path no writer can produce, and + /// no assertion about accepted names would see it. Compared as OUTCOMES: + /// each call either reverts or returns, and the revert data is compared too, + /// so the two also agree on WHICH name was the wrong one. That is what makes + /// the whole fuzz domain assertable rather than only the accepted part of + /// it, and it needs no restatement here of which names are accepted — a + /// restated rule is a third spelling, and the two being held together is the + /// entire point. /// @param dir The snapshot directory name. /// @param contractName The name of the contract. function testRootAwareSnapshotPathIsTheWritersAtTheRealRoot(string memory dir, string memory contractName) external - pure + view { - assertEq( - LibRainDeploySnapshot.pathForSnapshot(LibRainDeploySnapshot.LIB_FS_ROOT, dir, contractName), - LibRainDeploySnapshot.pathForSnapshot(dir, contractName) - ); + (bool rootAwareOk, bytes memory rootAware) = address(this) + .staticcall( + abi.encodeCall(this.externalPathForSnapshotAt, (LibRainDeploySnapshot.LIB_FS_ROOT, dir, contractName)) + ); + (bool writerOk, bytes memory writer) = + address(this).staticcall(abi.encodeCall(this.externalPathForSnapshot, (dir, contractName))); + + assertEq(rootAwareOk, writerOk, "one spelling accepted a name the other refused"); + assertEq(rootAware, writer); } - /// The root the record is WALKED from MUST be the root the writer WRITES - /// to. They are two constants — `LIB_FS_ROOT` here, and the one - /// `LibFs.pathForContract` hardcodes in a package this repo does not own — - /// and a walk of a root nothing writes to returns nothing, which every - /// record-anchored assertion then passes on. Silence is the failure mode, - /// so it is asserted rather than observed. + /// The record is WALKED from the root the writer WRITES to, and each of its + /// files is exactly `//.sol`. A walk of a root nothing + /// writes to returns nothing, which every record-anchored assertion then + /// passes on, and a snapshot one segment deeper or shallower than the walk + /// looks is the same silence. + /// + /// `LIB_FS_ROOT` is now `LibFs`'s own `GENERATED_DIR` rather than a second + /// constant held equal to it, so the root itself cannot diverge and is not + /// what this asserts. What it asserts is the LAYOUT the writer interpolates + /// that root into: `pathForSnapshot` and `dirForSnapshot` are both + /// `rain-sol-codegen`'s, in a package this repo does not own, so how deep a + /// snapshot lands and how deep the walk reads are two decisions in two repos + /// that a version bump can move independently. /// - /// Compared through `pathForSnapshot`, which is what a snapshot is written - /// through, so the right hand side is the writer's own root rather than a - /// third restatement of it. The contract name is arbitrary — the path is - /// built by concatenation and names no artifact. + /// The contract name is arbitrary — the path is built by concatenation and + /// names no artifact. function testRecordRootIsTheRootTheWriterWritesTo() external pure { assertEq( LibRainDeploySnapshot.pathForSnapshot("0_1_7", "Foo"), @@ -319,14 +368,26 @@ contract LibRainDeploySnapshotTest is Test { /// walks from other contracts that forge runs in parallel with this one, so /// a tag-shaped name here would be a release those contracts have to fail /// on for as long as it exists. + /// + /// Not tag SHAPED, but still drawn from `LibFs`'s tag ALPHABET, which the + /// writer requires of every directory it will place a file in. The two rules + /// are different: the alphabet is what makes a name safe to interpolate into + /// a path, and being a strict `X_Y_Z` triple on top of that is what makes it + /// a release. function testWriteSnapshotWritesTheSnapshotAtItsPath() external { - string memory dir = "write-snapshot-not-a-tag"; + string memory dir = "writeSnapshotNotATag"; assertFalse(LibRainDeploySnapshot.isTag(dir)); //forge-lint: disable-next-line(unsafe-cheatcode) vm.createDir(LibRainDeploySnapshot.dirForSnapshot(dir), true); string memory written = LibRainDeploySnapshot.writeSnapshot( - vm, dir, "MockDeployable", type(MockDeployable).creationCode, new address[](0) + vm, + dir, + "MockDeployable", + GENERATED_SPDX_LICENSE_IDENTIFIER, + GENERATED_COPYRIGHT_TEXT, + type(MockDeployable).creationCode, + new address[](0) ); // Read while the snapshot is still there, asserted once it is gone. bool exists = vm.exists(written); @@ -416,6 +477,13 @@ contract LibRainDeploySnapshotTest is Test { /// the suite can see that: the alias lib's VALUES are anchored by the pins /// tests that read them, and its TEXT by nothing at all. /// + /// Emitted with `script/Build.sol`'s OWN licence constants, so this is also + /// where those are held to producing the header the committed generated + /// files actually carry. They are the calling repo's to state and reach + /// `LibCodeGen.filePrefix` unchecked by anything else here — a wrong + /// identifier is otherwise only a `reuse lint` failure on a file that has + /// already been written and committed. + /// /// Restored BEFORE the assertions run, because forge-std assertions revert: /// restoring afterwards restores in every case except a failure, which is /// the only case where the tree is dirty and the one this test exists to @@ -424,7 +492,12 @@ contract LibRainDeploySnapshotTest is Test { string memory before = vm.readFile(ALIAS_LIB_PATH); string memory written = LibRainDeploySnapshot.writeAliasLib( - vm, "AddressRegistry", "ADDRESS_REGISTRY", LibRainDeploySnapshot.CANDIDATE + vm, + "AddressRegistry", + "ADDRESS_REGISTRY", + LibRainDeploySnapshot.CANDIDATE, + GENERATED_SPDX_LICENSE_IDENTIFIER, + GENERATED_COPYRIGHT_TEXT ); string memory emitted = vm.readFile(ALIAS_LIB_PATH); @@ -716,8 +789,10 @@ contract LibRainDeploySnapshotTest is Test { /// shaped, for the reason `testWriteSnapshotWritesTheSnapshotAtItsPath` /// gives: the record root is the real `src/generated/`, walked by the /// inherited record check in contracts forge runs in parallel with this - /// one, so a tag-shaped name here is a release they have to fail on. - string constant DEPENDENCIES_FIXTURE_DIR = "write-dependencies-not-a-tag"; + /// one, so a tag-shaped name here is a release they have to fail on. Drawn + /// from the tag alphabet even so, because the writer places files only in + /// directories whose names are. + string constant DEPENDENCIES_FIXTURE_DIR = "writeDependenciesNotATag"; /// Freezes `dependencies` into a snapshot and returns the source written. /// @@ -734,7 +809,13 @@ contract LibRainDeploySnapshotTest is Test { uint256 state = vm.snapshotState(); string memory source = vm.readFile( LibRainDeploySnapshot.writeSnapshot( - vm, DEPENDENCIES_FIXTURE_DIR, FIXTURE_CONTRACT, type(MockDeployable).creationCode, dependencies + vm, + DEPENDENCIES_FIXTURE_DIR, + FIXTURE_CONTRACT, + GENERATED_SPDX_LICENSE_IDENTIFIER, + GENERATED_COPYRIGHT_TEXT, + type(MockDeployable).creationCode, + dependencies ) ); assertTrue(vm.revertToState(state), "the deploy state did not roll back"); @@ -954,15 +1035,24 @@ contract LibRainDeploySnapshotTest is Test { string memory path = string.concat("src/lib/", libraryName, ".sol"); string memory written = LibRainDeploySnapshot.writeReleasedSuitesLib( - vm, RELEASED_FIXTURE_ROOT, FIXTURE_CONTRACT, emitterTemplate() + vm, + RELEASED_FIXTURE_ROOT, + FIXTURE_CONTRACT, + GENERATED_SPDX_LICENSE_IDENTIFIER, + GENERATED_COPYRIGHT_TEXT, + emitterTemplate() ); string memory emitted = vm.readFile(path); // Built while the fixture record is still there: both emitters read it. string memory expected = string.concat( "// SPDX-License", - "-Identifier: LicenseRef-DCL-1.0\n", - "// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd\n", + "-Identifier: ", + GENERATED_SPDX_LICENSE_IDENTIFIER, + "\n", + "// SPDX-FileCopyrightText: ", + GENERATED_COPYRIGHT_TEXT, + "\n", "pragma solidity ^0.8.25;\n\n", "// THIS FILE IS AUTOGENERATED BY THE BUILD SCRIPT. DO NOT EDIT BY HAND.\n\n", LibRainDeploySnapshot.releasedImportBlock(vm, paths), @@ -996,7 +1086,12 @@ contract LibRainDeploySnapshotTest is Test { string memory before = vm.readFile(path); string memory written = LibRainDeploySnapshot.writeReleasedSuitesLib( - vm, LibRainDeploySnapshot.LIB_FS_ROOT, EMITTED_CONTRACT, emitterTemplate() + vm, + LibRainDeploySnapshot.LIB_FS_ROOT, + EMITTED_CONTRACT, + GENERATED_SPDX_LICENSE_IDENTIFIER, + GENERATED_COPYRIGHT_TEXT, + emitterTemplate() ); string memory emitted = vm.readFile(path); @@ -1004,8 +1099,12 @@ contract LibRainDeploySnapshotTest is Test { LibRainDeploySnapshot.recordPathsForContract(vm, LibRainDeploySnapshot.LIB_FS_ROOT, EMITTED_CONTRACT); string memory expected = string.concat( "// SPDX-License", - "-Identifier: LicenseRef-DCL-1.0\n", - "// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd\n", + "-Identifier: ", + GENERATED_SPDX_LICENSE_IDENTIFIER, + "\n", + "// SPDX-FileCopyrightText: ", + GENERATED_COPYRIGHT_TEXT, + "\n", "pragma solidity ^0.8.25;\n\n", "// THIS FILE IS AUTOGENERATED BY THE BUILD SCRIPT. DO NOT EDIT BY HAND.\n\n", LibRainDeploySnapshot.releasedImportBlock(vm, paths), From dc3204f653fb84b14877cccf69723abf07cf86df Mon Sep 17 00:00:00 2001 From: David Meister Date: Mon, 17 Aug 2026 15:21:15 +0000 Subject: [PATCH 2/6] Assert the snapshot writer emits the header it was handed The licence and the copyright holder are parameters of `LibRainDeploySnapshot` rather than constants inside it, so that deploy repos in other orgs get their own header in their own append-only `src/generated//`. That freedom is only worth having if what is handed in is what comes out. The alias and released emitters each already assert their output byte for byte. The snapshot writer had no such assertion: the two values are strings of the same type in adjacent positions, so a call that swapped them compiles and writes a file carrying both, in the wrong tags. Every other test that reads a snapshot header reads the COMMITTED file, which is evidence about a generation that already happened rather than about what the writer emits now. Co-Authored-By: Claude Opus 5 (1M context) --- test/src/lib/LibRainDeploySnapshot.t.sol | 68 ++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/test/src/lib/LibRainDeploySnapshot.t.sol b/test/src/lib/LibRainDeploySnapshot.t.sol index 3348f77..af96da5 100644 --- a/test/src/lib/LibRainDeploySnapshot.t.sol +++ b/test/src/lib/LibRainDeploySnapshot.t.sol @@ -399,6 +399,74 @@ contract LibRainDeploySnapshotTest is Test { assertTrue(exists); } + /// The directory the licence-header fixture snapshot is written into. Not + /// tag shaped, for the reason `testWriteSnapshotWritesTheSnapshotAtItsPath` + /// gives, and drawn from the tag alphabet because the writer places files + /// only in directories whose names are. + string constant HEADER_FIXTURE_DIR = "writeSnapshotHeaderNotATag"; + + /// A written snapshot MUST declare the licence and the copyright holder it + /// was HANDED, each in its own tag. + /// + /// They are parameters of this library rather than constants inside it + /// because `rain-deploy` is published and deploy repos in other orgs consume + /// it, so a header chosen here would land in THEIR + /// `src/generated//` — append-only, so permanently. That freedom is + /// only worth having if what is handed in is what comes out, and the two are + /// strings of the same type in adjacent positions: a call that swapped them + /// compiles, and writes a file carrying both values, in the wrong tags. + /// + /// The alias and released emitters each have a byte-for-byte assertion that + /// covers this. The snapshot writer had none. Everything else in the suite + /// that reads a snapshot header reads the COMMITTED file, which is evidence + /// about a generation that already happened rather than about what the + /// writer emits now — so a swap here would reach a consumer's frozen record + /// with a green suite behind it. + /// + /// The expected text is spelled out rather than taken from + /// `LibCodeGen.filePrefix`, so the oracle does not come from the code that + /// wrote the file. `pragma` is the first byte of the header that is neither + /// parameter, so splitting there is the whole of what the two of them + /// produce — an assertion on the header ENTIRE rather than on either value + /// appearing somewhere in it. + /// + /// Split across `string.concat` so `reuse lint` does not read the expected + /// text as a licence declaration this test file is making about itself. + /// + /// Read before the fixture is removed and asserted after, because forge-std + /// assertions revert: cleaning up afterwards cleans up in every case except + /// a failure, which is the one case that leaves a directory under the real + /// `src/generated/` for every suite that walks it. + function testWriteSnapshotDeclaresTheLicenceItWasHanded() external { + string memory source = vm.readFile( + LibRainDeploySnapshot.writeSnapshot( + vm, + HEADER_FIXTURE_DIR, + FIXTURE_CONTRACT, + GENERATED_SPDX_LICENSE_IDENTIFIER, + GENERATED_COPYRIGHT_TEXT, + type(MockDeployable).creationCode, + new address[](0) + ) + ); + + //forge-lint: disable-next-line(unsafe-cheatcode) + vm.removeDir(LibRainDeploySnapshot.dirForSnapshot(HEADER_FIXTURE_DIR), true); + + assertEq( + vm.split(source, "pragma")[0], + string.concat( + "// SPDX-License", + "-Identifier: ", + GENERATED_SPDX_LICENSE_IDENTIFIER, + "\n", + "// SPDX-FileCopyrightText: ", + GENERATED_COPYRIGHT_TEXT, + "\n" + ) + ); + } + /// The alias lib this repo's `AddressRegistry` snapshot is re-exported /// through, and the only file the alias emitter writes here. /// From 47e58b1739e9daf05de3fdfdd349fb61904a862e Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 18 Aug 2026 09:34:01 +0000 Subject: [PATCH 3/6] Move to rain-sol-codegen 0.1.36 0.1.35 predates the licence defaults. 0.1.36 is the release that carries `RAIN_SPDX_LICENSE_IDENTIFIER` and `RAIN_COPYRIGHT_TEXT` and the arities of `filePrefix` and the `LibFs` writers that resolve to them. `soldeer update` adds the new remapping but leaves the superseded one in place, so `rain-sol-codegen-0.1.35/` is deleted here rather than left to resolve against a directory that no longer exists. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 6 +++--- foundry.toml | 2 +- remappings.txt | 2 +- soldeer.lock | 8 ++++---- src/lib/LibRainDeploySnapshot.sol | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4ce9f27..ec7387a 100644 --- a/README.md +++ b/README.md @@ -343,8 +343,8 @@ Via [soldeer](https://soldeer.xyz): forge soldeer install rain-deploy~ ``` -**You also need `forge-std` 1.16.2 and `rain-sol-codegen` 0.1.35**, remapped as -`forge-std-1.16.2/` and `rain-sol-codegen-0.1.35/`. The published package ships +**You also need `forge-std` 1.16.2 and `rain-sol-codegen` 0.1.36**, remapped as +`forge-std-1.16.2/` and `rain-sol-codegen-0.1.36/`. The published package ships `src/`, `script/` and the licence and README files — no `test/`, no `foundry.toml`, no `remappings.txt`, no `soldeer.lock`, no `dependencies/` — so a consumer resolves both itself. The requirement is transitive rather than @@ -357,7 +357,7 @@ every abstract a consumer inherits pulls them in — `Script` via ```toml [dependencies] forge-std = "1.16.2" -rain-sol-codegen = "0.1.35" +rain-sol-codegen = "0.1.36" rain-deploy = "" ``` diff --git a/foundry.toml b/foundry.toml index c3cb916..7a9c3ef 100644 --- a/foundry.toml +++ b/foundry.toml @@ -49,7 +49,7 @@ fs_permissions = [ [dependencies] forge-std = "1.16.2" -rain-sol-codegen = "0.1.35" +rain-sol-codegen = "0.1.36" [soldeer] recursive_deps = false diff --git a/remappings.txt b/remappings.txt index 24a4a7e..41b5726 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1,2 @@ forge-std-1.16.2/=dependencies/forge-std-1.16.2/ -rain-sol-codegen-0.1.35/=dependencies/rain-sol-codegen-0.1.35/ +rain-sol-codegen-0.1.36/=dependencies/rain-sol-codegen-0.1.36/ diff --git a/soldeer.lock b/soldeer.lock index 5129737..b451bed 100644 --- a/soldeer.lock +++ b/soldeer.lock @@ -7,7 +7,7 @@ integrity = "5fb4325b60d7d4194bd0481e2f94398cf1d9c6561dd12a66fb2748ef4a68205f" [[dependencies]] name = "rain-sol-codegen" -version = "0.1.35" -url = "https://soldeer-revisions.s3.amazonaws.com/rain-sol-codegen/0_1_35_17-08-2026_12:55:14_rain.sol.zip" -checksum = "b4f04fc3ce97461fda3702dfbc12a59cdd06d09e146fb5c81435d91f92a599ab" -integrity = "690dfad8d4f658fbdaf1a5ab94971bdbb34cba500d3d6f94b42088730b251f4c" +version = "0.1.36" +url = "https://soldeer-revisions.s3.amazonaws.com/rain-sol-codegen/0_1_36_18-08-2026_09:20:33_rain.sol.zip" +checksum = "b5d101d46941a95fee441224e101004139814ae9afa2793138874bd7e93991ff" +integrity = "164e2655ac8b66bcf52c799421706d3dfaab987addaaeadd74574e70fd2eb6ed" diff --git a/src/lib/LibRainDeploySnapshot.sol b/src/lib/LibRainDeploySnapshot.sol index 736b1da..777e82f 100644 --- a/src/lib/LibRainDeploySnapshot.sol +++ b/src/lib/LibRainDeploySnapshot.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.25; import {Vm} from "forge-std-1.16.2/src/Vm.sol"; -import {LibCodeGen} from "rain-sol-codegen-0.1.35/src/lib/LibCodeGen.sol"; -import {GENERATED_DIR, LibFs} from "rain-sol-codegen-0.1.35/src/lib/LibFs.sol"; +import {LibCodeGen} from "rain-sol-codegen-0.1.36/src/lib/LibCodeGen.sol"; +import {GENERATED_DIR, LibFs} from "rain-sol-codegen-0.1.36/src/lib/LibFs.sol"; import {DeploySuite} from "../abstract/RainDeploySuitesBase.sol"; import {LibRainDeploy} from "./LibRainDeploy.sol"; From ebfd900d10a6841f264a6abab121adb800fc01f6 Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 18 Aug 2026 09:41:55 +0000 Subject: [PATCH 4/6] Name codegen's header constants rather than restating them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `script/Build.sol` declared `GENERATED_SPDX_LICENSE_IDENTIFIER` and `GENERATED_COPYRIGHT_TEXT` as its own string literals, holding the same two values `rain-sol-codegen` 0.1.36 exports as `RAIN_SPDX_LICENSE_IDENTIFIER` and `RAIN_COPYRIGHT_TEXT`. Two definitions of one invariant, free to drift, with nothing comparing them. Both are deleted and the call sites — three in `Build.sol`, twelve in `LibRainDeploySnapshot.t.sol` — name codegen's. `LibRainDeploySnapshot` and `LibFs` still take the header as parameters, because both are dependencies of deploy repos in other orgs whose generated trees are append-only; what changes is only which repo the values are written down in. `testWriteAliasLibWritesTheLibAtItsPath` emits with these constants and asserts the result is the committed generated file byte for byte. It passes, and no file under `src/generated/` moves, so the emitted header is unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- script/Build.sol | 53 ++++++++++++------------ test/src/lib/LibRainDeploySnapshot.t.sol | 50 +++++++++++----------- 2 files changed, 52 insertions(+), 51 deletions(-) diff --git a/script/Build.sol b/script/Build.sol index f437d70..80dceec 100644 --- a/script/Build.sol +++ b/script/Build.sol @@ -3,30 +3,11 @@ pragma solidity =0.8.25; import {Script} from "forge-std-1.16.2/src/Script.sol"; +import {RAIN_COPYRIGHT_TEXT, RAIN_SPDX_LICENSE_IDENTIFIER} from "rain-sol-codegen-0.1.36/src/lib/LibCodeGen.sol"; import {DeployCandidate} from "../src/abstract/RainDeploySuitesBase.sol"; import {RegistryDeploySuites} from "../src/abstract/RegistryDeploySuites.sol"; import {LibRainDeploySnapshot} from "../src/lib/LibRainDeploySnapshot.sol"; -/// @dev The SPDX licence identifier every file this script generates declares. -/// -/// THIS repo's licence, declared by THIS repo, which is the whole point of it -/// being here. `LibRainDeploySnapshot` and `rain-sol-codegen` beneath it take it -/// as a parameter rather than choosing one, because both are dependencies of -/// deploy repos in other orgs, and a header either of them chose would land in -/// those repos' `src/generated//` — append-only, so permanently — under a -/// licence and a copyright holder that are not theirs. -/// -/// `reuse lint` reads the emitted files, so an identifier here with no text in -/// `LICENSES/` fails the legal job. What ties it to the header the committed -/// generated files actually carry is `testWriteAliasLibWritesTheLibAtItsPath`, -/// which emits with these and asserts the result is the committed file byte for -/// byte. -string constant GENERATED_SPDX_LICENSE_IDENTIFIER = "LicenseRef-DCL-1.0"; - -/// @dev The copyright holder every file this script generates names. This repo's, for -/// the reason `GENERATED_SPDX_LICENSE_IDENTIFIER` is. -string constant GENERATED_COPYRIGHT_TEXT = "Copyright (c) 2020 Rain Open Source Software Ltd"; - /// One contract's generated files: the rolling snapshot, the alias lib that /// re-exports its pins and the released-suites lib emitted from its record. /// @@ -102,6 +83,26 @@ struct GeneratedContract { /// generated-lib writers all come from `LibRainDeploySnapshot`, which in turn /// emits every constant through `LibCodeGen` and writes snapshots through /// `LibFs`. This script is the declaration and the sequencing, nothing else. +/// +/// ## The header the generated files declare +/// +/// Every file emitted here declares `RAIN_SPDX_LICENSE_IDENTIFIER` and +/// `RAIN_COPYRIGHT_TEXT`, named from `rain-sol-codegen` rather than restated, +/// so Rain's header has one definition and no second copy here to drift from +/// it. +/// +/// `LibRainDeploySnapshot` and `LibFs` beneath it take the header as parameters +/// rather than choosing one, because both are dependencies of deploy repos in +/// other orgs, and a header either of them chose would land in those repos' +/// `src/generated//` — append-only, so permanently — under a licence and a +/// copyright holder that are not theirs. This repo is one of the org's own, so +/// it names the org's values at the call site. +/// +/// `reuse lint` reads the emitted files, so an identifier with no text in +/// `LICENSES/` fails the legal job. What ties these to the header the committed +/// generated files actually carry is `testWriteAliasLibWritesTheLibAtItsPath`, +/// which emits with them and asserts the result is the committed file byte for +/// byte. contract Build is Script, RegistryDeploySuites { /// Every contract this repo generates deploy pins for, declared ONCE. /// @return contracts The generated contracts. @@ -158,15 +159,15 @@ contract Build is Script, RegistryDeploySuites { contracts[i].contractName, contracts[i].constantPrefix, LibRainDeploySnapshot.CANDIDATE, - GENERATED_SPDX_LICENSE_IDENTIFIER, - GENERATED_COPYRIGHT_TEXT + RAIN_SPDX_LICENSE_IDENTIFIER, + RAIN_COPYRIGHT_TEXT ); LibRainDeploySnapshot.writeReleasedSuitesLib( vm, LibRainDeploySnapshot.LIB_FS_ROOT, contracts[i].contractName, - GENERATED_SPDX_LICENSE_IDENTIFIER, - GENERATED_COPYRIGHT_TEXT, + RAIN_SPDX_LICENSE_IDENTIFIER, + RAIN_COPYRIGHT_TEXT, contracts[i].candidate.snapshot ); } @@ -181,8 +182,8 @@ contract Build is Script, RegistryDeploySuites { vm, LibRainDeploySnapshot.CANDIDATE, contracts[i].contractName, - GENERATED_SPDX_LICENSE_IDENTIFIER, - GENERATED_COPYRIGHT_TEXT, + RAIN_SPDX_LICENSE_IDENTIFIER, + RAIN_COPYRIGHT_TEXT, contracts[i].candidate.sourceCreationCode, contracts[i].candidate.snapshot.dependencies ); diff --git a/test/src/lib/LibRainDeploySnapshot.t.sol b/test/src/lib/LibRainDeploySnapshot.t.sol index af96da5..b91cabd 100644 --- a/test/src/lib/LibRainDeploySnapshot.t.sol +++ b/test/src/lib/LibRainDeploySnapshot.t.sol @@ -4,7 +4,7 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.2/src/Test.sol"; -import {GENERATED_COPYRIGHT_TEXT, GENERATED_SPDX_LICENSE_IDENTIFIER} from "../../../script/Build.sol"; +import {RAIN_COPYRIGHT_TEXT, RAIN_SPDX_LICENSE_IDENTIFIER} from "rain-sol-codegen-0.1.36/src/lib/LibCodeGen.sol"; import {DeploySuite} from "../../../src/abstract/RainDeploySuitesBase.sol"; import { EmptyRelease, @@ -384,8 +384,8 @@ contract LibRainDeploySnapshotTest is Test { vm, dir, "MockDeployable", - GENERATED_SPDX_LICENSE_IDENTIFIER, - GENERATED_COPYRIGHT_TEXT, + RAIN_SPDX_LICENSE_IDENTIFIER, + RAIN_COPYRIGHT_TEXT, type(MockDeployable).creationCode, new address[](0) ); @@ -443,8 +443,8 @@ contract LibRainDeploySnapshotTest is Test { vm, HEADER_FIXTURE_DIR, FIXTURE_CONTRACT, - GENERATED_SPDX_LICENSE_IDENTIFIER, - GENERATED_COPYRIGHT_TEXT, + RAIN_SPDX_LICENSE_IDENTIFIER, + RAIN_COPYRIGHT_TEXT, type(MockDeployable).creationCode, new address[](0) ) @@ -458,10 +458,10 @@ contract LibRainDeploySnapshotTest is Test { string.concat( "// SPDX-License", "-Identifier: ", - GENERATED_SPDX_LICENSE_IDENTIFIER, + RAIN_SPDX_LICENSE_IDENTIFIER, "\n", "// SPDX-FileCopyrightText: ", - GENERATED_COPYRIGHT_TEXT, + RAIN_COPYRIGHT_TEXT, "\n" ) ); @@ -545,12 +545,12 @@ contract LibRainDeploySnapshotTest is Test { /// the suite can see that: the alias lib's VALUES are anchored by the pins /// tests that read them, and its TEXT by nothing at all. /// - /// Emitted with `script/Build.sol`'s OWN licence constants, so this is also - /// where those are held to producing the header the committed generated - /// files actually carry. They are the calling repo's to state and reach - /// `LibCodeGen.filePrefix` unchecked by anything else here — a wrong - /// identifier is otherwise only a `reuse lint` failure on a file that has - /// already been written and committed. + /// Emitted with the same header constants `script/Build.sol` names, so this + /// is also where those are held to producing the header the committed + /// generated files actually carry. They reach `LibCodeGen.filePrefix` + /// unchecked by anything else here — a wrong identifier is otherwise only a + /// `reuse lint` failure on a file that has already been written and + /// committed. /// /// Restored BEFORE the assertions run, because forge-std assertions revert: /// restoring afterwards restores in every case except a failure, which is @@ -564,8 +564,8 @@ contract LibRainDeploySnapshotTest is Test { "AddressRegistry", "ADDRESS_REGISTRY", LibRainDeploySnapshot.CANDIDATE, - GENERATED_SPDX_LICENSE_IDENTIFIER, - GENERATED_COPYRIGHT_TEXT + RAIN_SPDX_LICENSE_IDENTIFIER, + RAIN_COPYRIGHT_TEXT ); string memory emitted = vm.readFile(ALIAS_LIB_PATH); @@ -880,8 +880,8 @@ contract LibRainDeploySnapshotTest is Test { vm, DEPENDENCIES_FIXTURE_DIR, FIXTURE_CONTRACT, - GENERATED_SPDX_LICENSE_IDENTIFIER, - GENERATED_COPYRIGHT_TEXT, + RAIN_SPDX_LICENSE_IDENTIFIER, + RAIN_COPYRIGHT_TEXT, type(MockDeployable).creationCode, dependencies ) @@ -1106,8 +1106,8 @@ contract LibRainDeploySnapshotTest is Test { vm, RELEASED_FIXTURE_ROOT, FIXTURE_CONTRACT, - GENERATED_SPDX_LICENSE_IDENTIFIER, - GENERATED_COPYRIGHT_TEXT, + RAIN_SPDX_LICENSE_IDENTIFIER, + RAIN_COPYRIGHT_TEXT, emitterTemplate() ); @@ -1116,10 +1116,10 @@ contract LibRainDeploySnapshotTest is Test { string memory expected = string.concat( "// SPDX-License", "-Identifier: ", - GENERATED_SPDX_LICENSE_IDENTIFIER, + RAIN_SPDX_LICENSE_IDENTIFIER, "\n", "// SPDX-FileCopyrightText: ", - GENERATED_COPYRIGHT_TEXT, + RAIN_COPYRIGHT_TEXT, "\n", "pragma solidity ^0.8.25;\n\n", "// THIS FILE IS AUTOGENERATED BY THE BUILD SCRIPT. DO NOT EDIT BY HAND.\n\n", @@ -1157,8 +1157,8 @@ contract LibRainDeploySnapshotTest is Test { vm, LibRainDeploySnapshot.LIB_FS_ROOT, EMITTED_CONTRACT, - GENERATED_SPDX_LICENSE_IDENTIFIER, - GENERATED_COPYRIGHT_TEXT, + RAIN_SPDX_LICENSE_IDENTIFIER, + RAIN_COPYRIGHT_TEXT, emitterTemplate() ); string memory emitted = vm.readFile(path); @@ -1168,10 +1168,10 @@ contract LibRainDeploySnapshotTest is Test { string memory expected = string.concat( "// SPDX-License", "-Identifier: ", - GENERATED_SPDX_LICENSE_IDENTIFIER, + RAIN_SPDX_LICENSE_IDENTIFIER, "\n", "// SPDX-FileCopyrightText: ", - GENERATED_COPYRIGHT_TEXT, + RAIN_COPYRIGHT_TEXT, "\n", "pragma solidity ^0.8.25;\n\n", "// THIS FILE IS AUTOGENERATED BY THE BUILD SCRIPT. DO NOT EDIT BY HAND.\n\n", From 05ee63934ddb74645d2c27b8d9030e9788011e8c Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 18 Aug 2026 09:44:37 +0000 Subject: [PATCH 5/6] Cut the exposition out of Build.sol's comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 192 lines of which 111 were comment. The doc block argued for the design — why one list rather than two, why candidates are reached by name, what would go wrong under alternatives nobody wrote — rather than saying what the script does. Same in the alias-lib test's NatSpec. Now 115 lines, 34 comment: what each entry point rewrites, what the struct fields place and name, what the test asserts. Co-Authored-By: Claude Opus 5 (1M context) --- script/Build.sol | 123 +++++------------------ test/src/lib/LibRainDeploySnapshot.t.sol | 20 +--- 2 files changed, 27 insertions(+), 116 deletions(-) diff --git a/script/Build.sol b/script/Build.sol index 80dceec..57701ed 100644 --- a/script/Build.sol +++ b/script/Build.sol @@ -10,21 +10,15 @@ import {LibRainDeploySnapshot} from "../src/lib/LibRainDeploySnapshot.sol"; /// One contract's generated files: the rolling snapshot, the alias lib that /// re-exports its pins and the released-suites lib emitted from its record. -/// -/// The candidate carries the creation code the snapshot is written from and the -/// declaration metadata the released lib copies, so the only things a generated -/// contract adds to it are the two names codegen needs. struct GeneratedContract { - /// The contract's name, which places its snapshot inside - /// `src/generated//` and names both generated libs. + /// Places the snapshot inside `src/generated//` and names both + /// generated libs. string contractName; - /// The prefix for the constants the alias lib exports, e.g. - /// `ADDRESS_REGISTRY`. Passed rather than derived; see `writeAliasLib`. + /// Prefix for the constants the alias lib exports, e.g. `ADDRESS_REGISTRY`. string constantPrefix; - /// The rolling candidate from the declaration. Its `sourceCreationCode` - /// and its `snapshot.dependencies` are what the snapshot is generated FROM - /// — both are frozen into it — and its `snapshot` is the template the - /// released lib takes its key and artifact path from. + /// Snapshots are written from its `sourceCreationCode` and + /// `snapshot.dependencies`; the released lib takes its suite key and + /// artifact path from its `snapshot`. DeployCandidate candidate; } @@ -32,79 +26,20 @@ struct GeneratedContract { /// @notice Generates the deterministic-deploy pins for every contract this repo /// deploys. /// -/// Two entry points, because there are two different things to do and only one -/// of them happens on an ordinary build: -/// -/// - `run()` — every build. Regenerates the ROLLING snapshots under -/// `src/generated/candidate/` from current source, and the alias libs that -/// point at them. Nothing here is frozen, so a source change simply moves it. -/// - `cutRelease()` — a release. Regenerates the rolling snapshots and freezes -/// them as `src/generated//`, in ONE call, in that order. -/// -/// The alias libs always point at `candidate`, so consumers' import paths never -/// move and `LibAddressRegistry` and `LibMigrationRegistry` always resolve -/// against what this repo currently compiles. The frozen `/` directories -/// are the historical record — what each release actually deployed — which is -/// what `RegistryDeploySuites.releasedSuites()` enumerates. -/// -/// BOTH entry points also regenerate the released-suites libs from the record. -/// `run()` must: they are imported by ordinary source, so a repo before its -/// first release still has to have them, and with nothing frozen they declare -/// an empty set. -/// -/// ## One list, three readers -/// -/// `generatedContracts()` is the whole of what this script declares, and the -/// regeneration, the lib writers and the freeze all read it. A contract added -/// to it is generated, aliased, released and frozen; there is no second list to -/// add it to and therefore no way to add it to one and not the other. That -/// matters most for the freeze: a contract regenerated but left out of the -/// names `freeze` is given is a contract silently absent from the release, -/// which nothing downstream can notice, because a tag that never held it has -/// nothing missing from it. -/// -/// The metadata each released entry carries beyond its frozen snapshot comes -/// from the named candidate on the declaration, which is why this inherits the -/// declaration rather than restating it. There is one suite key, one artifact -/// path and one dependency list per contract in this repo, and a second copy of -/// them here is a second copy that drifts. -/// -/// The dependency list goes into the SNAPSHOT rather than into the released lib -/// on every build. It is a precondition of the broadcast, not metadata, so a -/// release keeps the list it was cut with. `freeze` regenerates before it -/// freezes, so the list a release records is the declaration's as of the cut. -/// -/// Candidates are reached by NAME rather than by index into `candidateSuites()` -/// for the same reason: a released-suites lib describes one contract, so a -/// positional read would silently write another contract's metadata the moment -/// the list is reordered. -/// -/// The tag, both snapshot paths, the freeze, the snapshot writer and both -/// generated-lib writers all come from `LibRainDeploySnapshot`, which in turn -/// emits every constant through `LibCodeGen` and writes snapshots through -/// `LibFs`. This script is the declaration and the sequencing, nothing else. -/// -/// ## The header the generated files declare -/// -/// Every file emitted here declares `RAIN_SPDX_LICENSE_IDENTIFIER` and -/// `RAIN_COPYRIGHT_TEXT`, named from `rain-sol-codegen` rather than restated, -/// so Rain's header has one definition and no second copy here to drift from -/// it. +/// - `run()` rewrites the rolling snapshots under `src/generated/candidate/`, +/// the alias libs pointing at them, and the released-suites libs. +/// - `cutRelease()` does the same, freezing the rolling snapshots as +/// `src/generated//` in between. /// -/// `LibRainDeploySnapshot` and `LibFs` beneath it take the header as parameters -/// rather than choosing one, because both are dependencies of deploy repos in -/// other orgs, and a header either of them chose would land in those repos' -/// `src/generated//` — append-only, so permanently — under a licence and a -/// copyright holder that are not theirs. This repo is one of the org's own, so -/// it names the org's values at the call site. +/// Alias libs always point at `candidate`, so `LibAddressRegistry` and +/// `LibMigrationRegistry` resolve against what this repo currently compiles. +/// The frozen `/` directories are what +/// `RegistryDeploySuites.releasedSuites()` enumerates. /// -/// `reuse lint` reads the emitted files, so an identifier with no text in -/// `LICENSES/` fails the legal job. What ties these to the header the committed -/// generated files actually carry is `testWriteAliasLibWritesTheLibAtItsPath`, -/// which emits with them and asserts the result is the committed file byte for -/// byte. +/// `generatedContracts()` is the only list, read by the regeneration, both lib +/// writers and the freeze. contract Build is Script, RegistryDeploySuites { - /// Every contract this repo generates deploy pins for, declared ONCE. + /// Every contract this repo generates deploy pins for. /// @return contracts The generated contracts. function generatedContracts() internal pure returns (GeneratedContract[] memory contracts) { contracts = new GeneratedContract[](2); @@ -118,26 +53,16 @@ contract Build is Script, RegistryDeploySuites { }); } - /// @notice Every build: regenerate the rolling snapshots, their alias libs - /// and the released-suites libs. + /// @notice Regenerate the rolling snapshots, their alias libs and the + /// released-suites libs. function run() external { regenerateCandidates(); regenerateLibs(); } - /// @notice A release: regenerate the rolling snapshots, freeze them as this - /// release's immutable record, then regenerate the declaration of that - /// record. - /// - /// One invocation, so the ordering is a property of the tool rather than of - /// whoever wrote the release command. `LibRainDeploySnapshot.freeze` takes - /// the regeneration and runs it FIRST; there is no entry point that freezes - /// without regenerating, so a stale freeze has nowhere to come from. - /// - /// The released-suites libs are written from the record AFTER the freeze, - /// so the release being cut is in them. A frozen tag no released suite - /// declares is a release that drops out of every check there is, which is - /// exactly what generating the two from one call removes. + /// @notice Regenerate the rolling snapshots, freeze them as + /// `src/generated//`, then rewrite the libs from the record, so the + /// release being cut is in them. function cutRelease() external { GeneratedContract[] memory contracts = generatedContracts(); string[] memory contractNames = new string[](contracts.length); @@ -148,9 +73,7 @@ contract Build is Script, RegistryDeploySuites { regenerateLibs(); } - /// @notice Rewrite every alias lib and every released-suites lib. Both - /// entry points end here, so there is no entry point that regenerates one - /// and not the other. + /// @notice Rewrite every alias lib and every released-suites lib. function regenerateLibs() internal { GeneratedContract[] memory contracts = generatedContracts(); for (uint256 i = 0; i < contracts.length; i++) { diff --git a/test/src/lib/LibRainDeploySnapshot.t.sol b/test/src/lib/LibRainDeploySnapshot.t.sol index b91cabd..cbbb2b8 100644 --- a/test/src/lib/LibRainDeploySnapshot.t.sol +++ b/test/src/lib/LibRainDeploySnapshot.t.sol @@ -539,23 +539,11 @@ contract LibRainDeploySnapshotTest is Test { /// holding exactly the header, import block and library block the emitters /// produce. /// - /// Run against this repo's REAL contract and its real rolling snapshot, so - /// what it writes is the committed generated file and the assertion is that - /// the committed file IS what the generator emits today. Nothing else in - /// the suite can see that: the alias lib's VALUES are anchored by the pins - /// tests that read them, and its TEXT by nothing at all. + /// Runs against this repo's real contract and rolling snapshot, so it + /// overwrites the committed generated file and asserts that file is what + /// the generator emits today — header constants included. /// - /// Emitted with the same header constants `script/Build.sol` names, so this - /// is also where those are held to producing the header the committed - /// generated files actually carry. They reach `LibCodeGen.filePrefix` - /// unchecked by anything else here — a wrong identifier is otherwise only a - /// `reuse lint` failure on a file that has already been written and - /// committed. - /// - /// Restored BEFORE the assertions run, because forge-std assertions revert: - /// restoring afterwards restores in every case except a failure, which is - /// the only case where the tree is dirty and the one this test exists to - /// report. + /// The file is restored BEFORE the assertions, which revert on failure. function testWriteAliasLibWritesTheLibAtItsPath() external { string memory before = vm.readFile(ALIAS_LIB_PATH); From 0514369c8ac973c2188732c93237ccb687668cd9 Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 18 Aug 2026 09:54:01 +0000 Subject: [PATCH 6/6] Default the header in LibRainDeploySnapshot, not just in codegen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codegen 0.1.36 defaults the header at its own writers, but every caller reaches them through this library, whose three writers each had one arity requiring the licence and copyright. So a repo in this org still had to name the values, and `script/Build.sol` did — which is the same either/or the codegen change already removed one layer down: taking the values as parameters is what a repo outside this org needs, and it never ruled out an arity that defaults. `writeSnapshot`, `writeAliasLib` and `writeReleasedSuitesLib` each gain the defaulting overload. Build.sol drops the arguments and the import; nothing in this repo now names a licence in the build path. Covered three ways: the two tests that emit over the committed generated files and assert byte equality now go through the defaulting arity, and one test per writer asserts the defaulting arity writes exactly what the parameterised one writes when handed the org values — which a swapped or wrong constant breaks. `testWriteSnapshotDefaultsToTheOrgHeader` reverts EVM state between its two writes: the Zoltu deploy is CREATE2, so writing one creation code twice lands on an address that already has code. 209 passed, up from 206. Co-Authored-By: Claude Opus 5 (1M context) --- script/Build.sol | 17 +--- src/lib/LibRainDeploySnapshot.sol | 70 +++++++++++++-- test/src/lib/LibRainDeploySnapshot.t.sol | 105 ++++++++++++++++++++--- 3 files changed, 158 insertions(+), 34 deletions(-) diff --git a/script/Build.sol b/script/Build.sol index 57701ed..25997f0 100644 --- a/script/Build.sol +++ b/script/Build.sol @@ -3,7 +3,6 @@ pragma solidity =0.8.25; import {Script} from "forge-std-1.16.2/src/Script.sol"; -import {RAIN_COPYRIGHT_TEXT, RAIN_SPDX_LICENSE_IDENTIFIER} from "rain-sol-codegen-0.1.36/src/lib/LibCodeGen.sol"; import {DeployCandidate} from "../src/abstract/RainDeploySuitesBase.sol"; import {RegistryDeploySuites} from "../src/abstract/RegistryDeploySuites.sol"; import {LibRainDeploySnapshot} from "../src/lib/LibRainDeploySnapshot.sol"; @@ -78,20 +77,10 @@ contract Build is Script, RegistryDeploySuites { GeneratedContract[] memory contracts = generatedContracts(); for (uint256 i = 0; i < contracts.length; i++) { LibRainDeploySnapshot.writeAliasLib( - vm, - contracts[i].contractName, - contracts[i].constantPrefix, - LibRainDeploySnapshot.CANDIDATE, - RAIN_SPDX_LICENSE_IDENTIFIER, - RAIN_COPYRIGHT_TEXT + vm, contracts[i].contractName, contracts[i].constantPrefix, LibRainDeploySnapshot.CANDIDATE ); LibRainDeploySnapshot.writeReleasedSuitesLib( - vm, - LibRainDeploySnapshot.LIB_FS_ROOT, - contracts[i].contractName, - RAIN_SPDX_LICENSE_IDENTIFIER, - RAIN_COPYRIGHT_TEXT, - contracts[i].candidate.snapshot + vm, LibRainDeploySnapshot.LIB_FS_ROOT, contracts[i].contractName, contracts[i].candidate.snapshot ); } } @@ -105,8 +94,6 @@ contract Build is Script, RegistryDeploySuites { vm, LibRainDeploySnapshot.CANDIDATE, contracts[i].contractName, - RAIN_SPDX_LICENSE_IDENTIFIER, - RAIN_COPYRIGHT_TEXT, contracts[i].candidate.sourceCreationCode, contracts[i].candidate.snapshot.dependencies ); diff --git a/src/lib/LibRainDeploySnapshot.sol b/src/lib/LibRainDeploySnapshot.sol index 777e82f..f3b4749 100644 --- a/src/lib/LibRainDeploySnapshot.sol +++ b/src/lib/LibRainDeploySnapshot.sol @@ -3,7 +3,11 @@ pragma solidity ^0.8.25; import {Vm} from "forge-std-1.16.2/src/Vm.sol"; -import {LibCodeGen} from "rain-sol-codegen-0.1.36/src/lib/LibCodeGen.sol"; +import { + LibCodeGen, + RAIN_COPYRIGHT_TEXT, + RAIN_SPDX_LICENSE_IDENTIFIER +} from "rain-sol-codegen-0.1.36/src/lib/LibCodeGen.sol"; import {GENERATED_DIR, LibFs} from "rain-sol-codegen-0.1.36/src/lib/LibFs.sol"; import {DeploySuite} from "../abstract/RainDeploySuitesBase.sol"; import {LibRainDeploy} from "./LibRainDeploy.sol"; @@ -404,12 +408,9 @@ library LibRainDeploySnapshot { /// `abi.encode`d because Solidity has no file-scope constant of dynamic /// array type. The consumer is `releasedLibraryBlock`, which emits the /// matching `abi.decode`. - /// The licence and the copyright holder are the CALLING repo's, so they are - /// parameters here exactly as they are parameters of `LibCodeGen.filePrefix` - /// beneath. A snapshot is a file in the consuming repo's own tree, and this - /// library is a dependency of deploy repos that are not this one — a header - /// decided here would stamp this repo's licence into theirs, permanently, - /// into the one part of their tree that is append-only. + /// A snapshot lands in the calling repo's own tree, so the header is that + /// repo's statement. Repos outside this org call this overload; repos + /// inside it call the one that defaults to the org's values. /// @param vm The Vm instance for file operations. /// @param dir The snapshot directory name — a release tag, or `CANDIDATE`. /// @param contractName The contract the snapshot describes. @@ -443,6 +444,27 @@ library LibRainDeploySnapshot { return pathForSnapshot(dir, contractName); } + /// `writeSnapshot` applied to `RAIN_SPDX_LICENSE_IDENTIFIER` and + /// `RAIN_COPYRIGHT_TEXT`, for a repo this org owns. + /// @param vm The Vm instance for file operations. + /// @param dir The snapshot directory name — a release tag, or `CANDIDATE`. + /// @param contractName The contract the snapshot describes. + /// @param creationCode That contract's creation code. + /// @param dependencies The addresses that must already have code on a + /// network before this contract can be broadcast there. + /// @return The path written. + function writeSnapshot( + Vm vm, + string memory dir, + string memory contractName, + bytes memory creationCode, + address[] memory dependencies + ) internal returns (string memory) { + return writeSnapshot( + vm, dir, contractName, RAIN_SPDX_LICENSE_IDENTIFIER, RAIN_COPYRIGHT_TEXT, creationCode, dependencies + ); + } + /// The import block of a generated alias lib. /// @param contractName The contract the snapshot describes. /// @param constantPrefix The prefix for the emitted constants. @@ -551,6 +573,20 @@ library LibRainDeploySnapshot { return path; } + /// `writeAliasLib` applied to `RAIN_SPDX_LICENSE_IDENTIFIER` and + /// `RAIN_COPYRIGHT_TEXT`, for a repo this org owns. + /// @param vm The Vm instance for file operations. + /// @param contractName The contract the alias lib is written for. + /// @param constantPrefix The prefix for the emitted constants. + /// @param dir The snapshot directory to alias. + /// @return The path written. + function writeAliasLib(Vm vm, string memory contractName, string memory constantPrefix, string memory dir) + internal + returns (string memory) + { + return writeAliasLib(vm, contractName, constantPrefix, dir, RAIN_SPDX_LICENSE_IDENTIFIER, RAIN_COPYRIGHT_TEXT); + } + /// The release tag a record path sits under. /// @param vm The Vm instance for string operations. /// @param path A record file, as `frozenSnapshotPaths` returns it. @@ -912,6 +948,26 @@ library LibRainDeploySnapshot { return path; } + /// `writeReleasedSuitesLib` applied to `RAIN_SPDX_LICENSE_IDENTIFIER` and + /// `RAIN_COPYRIGHT_TEXT`, for a repo this org owns. + /// @param vm The Vm instance for file operations. + /// @param recordRoot The record root — `LIB_FS_ROOT` for a repo's real + /// record. + /// @param contractName The contract the released lib is written for. + /// @param template The suite the released entries take their key and + /// artifact path from. + /// @return The path written. + function writeReleasedSuitesLib( + Vm vm, + string memory recordRoot, + string memory contractName, + DeploySuite memory template + ) internal returns (string memory) { + return writeReleasedSuitesLib( + vm, recordRoot, contractName, RAIN_SPDX_LICENSE_IDENTIFIER, RAIN_COPYRIGHT_TEXT, template + ); + } + /// The newest release in a record: the greatest tag any of its files sits /// under, compared as a version. /// diff --git a/test/src/lib/LibRainDeploySnapshot.t.sol b/test/src/lib/LibRainDeploySnapshot.t.sol index cbbb2b8..99ec771 100644 --- a/test/src/lib/LibRainDeploySnapshot.t.sol +++ b/test/src/lib/LibRainDeploySnapshot.t.sol @@ -548,12 +548,7 @@ contract LibRainDeploySnapshotTest is Test { string memory before = vm.readFile(ALIAS_LIB_PATH); string memory written = LibRainDeploySnapshot.writeAliasLib( - vm, - "AddressRegistry", - "ADDRESS_REGISTRY", - LibRainDeploySnapshot.CANDIDATE, - RAIN_SPDX_LICENSE_IDENTIFIER, - RAIN_COPYRIGHT_TEXT + vm, "AddressRegistry", "ADDRESS_REGISTRY", LibRainDeploySnapshot.CANDIDATE ); string memory emitted = vm.readFile(ALIAS_LIB_PATH); @@ -1142,12 +1137,7 @@ contract LibRainDeploySnapshotTest is Test { string memory before = vm.readFile(path); string memory written = LibRainDeploySnapshot.writeReleasedSuitesLib( - vm, - LibRainDeploySnapshot.LIB_FS_ROOT, - EMITTED_CONTRACT, - RAIN_SPDX_LICENSE_IDENTIFIER, - RAIN_COPYRIGHT_TEXT, - emitterTemplate() + vm, LibRainDeploySnapshot.LIB_FS_ROOT, EMITTED_CONTRACT, emitterTemplate() ); string memory emitted = vm.readFile(path); @@ -1671,4 +1661,95 @@ contract LibRainDeploySnapshotTest is Test { } this.externalCheckReleaseFollowsRecord(FROZEN_FIXTURE_ROOT, tag); } + + /// The defaulting `writeSnapshot` MUST write exactly what the parameterised + /// one writes when handed this org's two values, in that order. A swap or a + /// wrong constant is otherwise only visible as a header nobody reads. + /// + /// State is reverted between the two writes because the Zoltu deploy is + /// CREATE2: writing one creation code twice would otherwise land on an + /// address that already has code. The files are on disk, so they outlive + /// the revert. + function testWriteSnapshotDefaultsToTheOrgHeader() external { + string memory dir = "writeSnapshotDefaults"; + //forge-lint: disable-next-line(unsafe-cheatcode) + vm.createDir(LibRainDeploySnapshot.dirForSnapshot(dir), true); + + uint256 undeployed = vm.snapshotState(); + string memory defaulted = vm.readFile( + LibRainDeploySnapshot.writeSnapshot( + vm, dir, "MockDeployable", type(MockDeployable).creationCode, new address[](0) + ) + ); + vm.revertToState(undeployed); + string memory explicitly = vm.readFile( + LibRainDeploySnapshot.writeSnapshot( + vm, + dir, + "MockDeployable", + RAIN_SPDX_LICENSE_IDENTIFIER, + RAIN_COPYRIGHT_TEXT, + type(MockDeployable).creationCode, + new address[](0) + ) + ); + + //forge-lint: disable-next-line(unsafe-cheatcode) + vm.removeDir(LibRainDeploySnapshot.dirForSnapshot(dir), true); + + assertEq(defaulted, explicitly); + } + + /// As `testWriteSnapshotDefaultsToTheOrgHeader`, for the alias lib. + function testWriteAliasLibDefaultsToTheOrgHeader() external { + string memory before = vm.readFile(ALIAS_LIB_PATH); + + string memory defaulted = vm.readFile( + LibRainDeploySnapshot.writeAliasLib( + vm, "AddressRegistry", "ADDRESS_REGISTRY", LibRainDeploySnapshot.CANDIDATE + ) + ); + string memory explicitly = vm.readFile( + LibRainDeploySnapshot.writeAliasLib( + vm, + "AddressRegistry", + "ADDRESS_REGISTRY", + LibRainDeploySnapshot.CANDIDATE, + RAIN_SPDX_LICENSE_IDENTIFIER, + RAIN_COPYRIGHT_TEXT + ) + ); + + //forge-lint: disable-next-line(unsafe-cheatcode) + vm.writeFile(ALIAS_LIB_PATH, before); + + assertEq(defaulted, explicitly); + } + + /// As `testWriteSnapshotDefaultsToTheOrgHeader`, for the released lib. + function testWriteReleasedSuitesLibDefaultsToTheOrgHeader() external { + string memory path = "src/lib/LibAddressRegistryReleased.sol"; + string memory before = vm.readFile(path); + + string memory defaulted = vm.readFile( + LibRainDeploySnapshot.writeReleasedSuitesLib( + vm, LibRainDeploySnapshot.LIB_FS_ROOT, EMITTED_CONTRACT, emitterTemplate() + ) + ); + string memory explicitly = vm.readFile( + LibRainDeploySnapshot.writeReleasedSuitesLib( + vm, + LibRainDeploySnapshot.LIB_FS_ROOT, + EMITTED_CONTRACT, + RAIN_SPDX_LICENSE_IDENTIFIER, + RAIN_COPYRIGHT_TEXT, + emitterTemplate() + ) + ); + + //forge-lint: disable-next-line(unsafe-cheatcode) + vm.writeFile(path, before); + + assertEq(defaulted, explicitly); + } }