Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ abstract contract MyDeploySuites is RainDeploySuitesBase {
// script/Deploy.sol
contract Deploy is MyDeploySuites, RainDeployBroadcast {}

// script/Build.sol
contract Build is MyDeploySuites, BuildScript {
function regenerateSnapshots() internal override;
function regenerateLibs() internal override;
function snapshotContractNames() internal view override returns (string[] memory);
}

// test/src/abstract/MyDeploySnapshot.t.sol
contract MyDeploySnapshotTest is MyDeploySuites, RainDeployVerifySnapshot {}

Expand All @@ -76,6 +83,12 @@ can be true — not because something checks for it, but because there is nothin
for it to disagree with. A repo that wrote its suites out twice would have that
bug available to it; this one does not.

`BuildScript` carries both build entry points concrete. `run()` regenerates the
generated sources and freezes nothing; `cutRelease()` regenerates, freezes the
release as `src/generated/<tag>/`, then regenerates from the record that now
holds it. Neither is `virtual`, so the entry point CI runs on every push has no
way to cut a release.

Suites are a **registry the abstract iterates**, not a chain of `else if`.
Adding a suite is adding an array entry. A mistyped `DEPLOYMENT_SUITE` reports
the valid keys built from that same array, so the error cannot fall behind the
Expand Down Expand Up @@ -391,8 +404,8 @@ codehashes consumers pin, so releases are **manual `sol-v*` tags**, not merges.
`[external.package].version` is the version of the LAST Soldeer publish, and
only a release moves it. A release cut under this lifecycle also names the
frozen `src/generated/<tag>/` record `cutRelease()` wrote for it. Every version
published under the previous merge-driven lifecycle predates that record and
has none, so `src/generated/` holds no directory for it; those versions stay
published under the previous merge-driven lifecycle predates that record and has
none, so `src/generated/` holds no directory for it; those versions stay
published, and consumers pin exact versions and are unaffected.

## Install
Expand Down
56 changes: 16 additions & 40 deletions script/Build.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.2/src/Script.sol";
import {BuildScript} from "../src/abstract/BuildScript.sol";
import {DeployCandidate} from "../src/abstract/RainDeploySuitesBase.sol";
import {RegistryDeploySuites} from "../src/abstract/RegistryDeploySuites.sol";
import {LibRainDeploySnapshot} from "../src/lib/LibRainDeploySnapshot.sol";
Expand All @@ -23,22 +23,15 @@ struct GeneratedContract {

/// @title Build
/// @notice Generates the deterministic-deploy pins for every contract this repo
/// deploys.
///
/// - `run()` rewrites the rolling snapshots under `src/generated/candidate/`,
/// the alias libs pointing at them, the released-suites libs and the
/// aggregate over them.
/// - `cutRelease()` does the same, freezing the rolling snapshots as
/// `src/generated/<tag>/` in between.
/// deploys. `run()` and `cutRelease()` are inherited from `BuildScript`.
///
/// Alias libs always point at `candidate`, so `LibAddressRegistry` and
/// `LibMigrationRegistry` resolve against what this repo currently compiles.
/// The frozen `<tag>/` directories are what
/// `RegistryDeploySuites.releasedSuites()` enumerates.
///
/// `generatedContracts()` is the only list, read by the regeneration, all three
/// lib writers and the freeze.
contract Build is Script, RegistryDeploySuites {
/// `generatedContracts()` is the only list, read by every hook below.
contract Build is BuildScript, RegistryDeploySuites {
/// Every contract this repo generates deploy pins for.
/// @return The generated contracts.
function generatedContracts() internal pure returns (GeneratedContract[] memory) {
Expand All @@ -54,10 +47,10 @@ contract Build is Script, RegistryDeploySuites {
return contracts;
}

/// Every generated contract's name, in declaration order — the order the
/// aggregate emits its entries in. Read by the freeze and the aggregate.
/// @return The contract names.
function generatedContractNames() internal pure returns (string[] memory) {
/// @inheritdoc BuildScript
/// @dev In declaration order — the order the aggregate emits its entries
/// in. Read by the freeze and the aggregate.
function snapshotContractNames() internal pure override returns (string[] memory) {
GeneratedContract[] memory contracts = generatedContracts();
string[] memory names = new string[](contracts.length);
for (uint256 i = 0; i < contracts.length; i++) {
Expand All @@ -66,41 +59,24 @@ contract Build is Script, RegistryDeploySuites {
return names;
}

/// @notice Regenerate the rolling snapshots, their alias libs, the
/// released-suites libs and the aggregate over them.
function run() external {
regenerateCandidates();
regenerateLibs();
}

/// @notice Regenerate the rolling snapshots, freeze them as
/// `src/generated/<tag>/`, then rewrite the libs from the record, so the
/// release being cut is in them.
function cutRelease() external {
LibRainDeploySnapshot.freeze(
vm, LibRainDeploySnapshot.LIB_FS_ROOT, regenerateCandidates, generatedContractNames()
);
regenerateLibs();
}

/// @notice Rewrite every alias lib, every released-suites lib and the
/// aggregate over them.
function regenerateLibs() internal {
/// @inheritdoc BuildScript
/// @dev Every alias lib, every released-suites lib and the aggregate over
/// them.
function regenerateLibs() internal override {
GeneratedContract[] memory contracts = generatedContracts();
for (uint256 i = 0; i < contracts.length; i++) {
LibRainDeploySnapshot.writeAliasLib(
vm, contracts[i].contractName, contracts[i].constantPrefix, LibRainDeploySnapshot.CANDIDATE
);
LibRainDeploySnapshot.writeReleasedSuitesLib(
vm, LibRainDeploySnapshot.LIB_FS_ROOT, contracts[i].contractName, contracts[i].candidate.snapshot
vm, recordRoot(), contracts[i].contractName, contracts[i].candidate.snapshot
);
}
LibRainDeploySnapshot.writeReleasedSuitesAggregate(vm, LibRainDeploySnapshot.LIB_DIR, generatedContractNames());
LibRainDeploySnapshot.writeReleasedSuitesAggregate(vm, LibRainDeploySnapshot.LIB_DIR, snapshotContractNames());
}

/// @notice Rewrite every `src/generated/candidate/` snapshot from what this
/// repo currently compiles.
function regenerateCandidates() internal {
/// @inheritdoc BuildScript
function regenerateSnapshots() internal override {
GeneratedContract[] memory contracts = generatedContracts();
for (uint256 i = 0; i < contracts.length; i++) {
LibRainDeploySnapshot.writeSnapshot(
Expand Down
2 changes: 1 addition & 1 deletion slither.config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"filter_paths": "dependencies/forge-std-|src/abstract/(RainDeploy(SuitesBase|Broadcast|VerifyBase|VerifyChain|VerifySnapshot)|RegistryDeploySuites)\\.sol",
"filter_paths": "dependencies/forge-std-|src/abstract/(RainDeploy(SuitesBase|Broadcast|VerifyBase|VerifyChain|VerifySnapshot)|RegistryDeploySuites|BuildScript)\\.sol",
"detectors_to_exclude": "assembly"
}
57 changes: 57 additions & 0 deletions src/abstract/BuildScript.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity ^0.8.25;

import {Script} from "forge-std-1.16.2/src/Script.sol";
import {LibRainDeploySnapshot} from "../lib/LibRainDeploySnapshot.sol";

/// @title BuildScript
/// @notice The two entry points of a deploy repo's `script/Build.sol`, concrete
/// here:
///
/// - `run()` regenerates and freezes nothing. This is the one CI runs.
/// - `cutRelease()` regenerates, freezes the rolling snapshots as
/// `<recordRoot()>/<tag>/`, then regenerates the libs from the record that
/// now holds the release being cut.
///
/// Neither is `virtual`, so a repo inheriting this implements the hooks below
/// and has no entry point to cut a release from other than `cutRelease()`.
abstract contract BuildScript is Script {
/// Rewrite the rolling `candidate/` snapshots from what this repo currently
/// compiles. Run by `cutRelease()` inside `freeze`, after its guards and
/// before it copies anything.
function regenerateSnapshots() internal virtual;

/// Rewrite every file generated from the snapshots and from the frozen
/// record. Run last, after a `cutRelease()` freeze has written the record,
/// so a file emitted from that record holds the release just cut.
function regenerateLibs() internal virtual;

/// The contracts whose rolling snapshots a release freezes.
/// @return The contract names.
function snapshotContractNames() internal view virtual returns (string[] memory);

/// The record root a release is frozen into, and the one the rolling
/// snapshots are read from.
///
/// Overridable so a `cutRelease()` can be exercised against a record other
/// than the repo's own, which is append-only and cannot hold a test's
/// release.
/// @return The record root.
function recordRoot() internal view virtual returns (string memory) {
return LibRainDeploySnapshot.LIB_FS_ROOT;
}

/// @notice Regenerate everything this repo generates. Freezes nothing.
function run() external {
regenerateSnapshots();
regenerateLibs();
}

/// @notice Regenerate the rolling snapshots, freeze them as this release's
/// record, then regenerate the libs from the record.
function cutRelease() external {
LibRainDeploySnapshot.freeze(vm, recordRoot(), regenerateSnapshots, snapshotContractNames());
regenerateLibs();
}
}
8 changes: 4 additions & 4 deletions test/concrete/BuildHarness.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ contract BuildHarness is Build {
return generatedContracts();
}

/// The name list the freeze and the aggregate are emitted from.
/// @return The generated contract names.
function externalGeneratedContractNames() external pure returns (string[] memory) {
return generatedContractNames();
/// The names a release cut from this script freezes.
/// @return The snapshot contract names.
function externalSnapshotContractNames() external pure returns (string[] memory) {
return snapshotContractNames();
}

/// The deploy declaration's list, through the same guarded reader every
Expand Down
121 changes: 121 additions & 0 deletions test/concrete/BuildScriptHarness.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity =0.8.25;

import {BuildScript} from "../../src/abstract/BuildScript.sol";
import {LibRainDeploySnapshot} from "../../src/lib/LibRainDeploySnapshot.sol";

/// @title BuildScriptHarness
/// @notice A `BuildScript` whose hooks write markers into a fixture record
/// instead of real generated sources, so `run()` and `cutRelease()` can be
/// called and what each one wrote — and what the record held when it wrote it —
/// read back.
///
/// The markers are comment-only `.sol` files, because a failing test leaves its
/// fixture behind and `forge test` compiles everything under `test/`.
contract BuildScriptHarness is BuildScript {
/// The fixture record root. Empty defers to `BuildScript`'s own.
string internal sRoot;

/// The single contract this fixture release freezes.
string internal sContractName;

/// @param root The fixture record root, or empty for `BuildScript`'s own.
/// @param contractName The contract the fixture snapshot describes.
constructor(string memory root, string memory contractName) {
sRoot = root;
sContractName = contractName;
}

/// @inheritdoc BuildScript
function recordRoot() internal view override returns (string memory) {
return bytes(sRoot).length > 0 ? sRoot : super.recordRoot();
}

/// The root a release cut from this harness is frozen into.
/// @return The record root.
function externalRecordRoot() external view returns (string memory) {
return recordRoot();
}

/// Where `regenerateSnapshots` writes.
/// @return The rolling snapshot path.
function rollingPath() public view returns (string memory) {
return LibRainDeploySnapshot.pathForSnapshot(recordRoot(), LibRainDeploySnapshot.CANDIDATE, sContractName);
}

/// Where the release freezes that snapshot to.
/// @return The frozen snapshot path.
function frozenPath() external view returns (string memory) {
return LibRainDeploySnapshot.pathForSnapshot(recordRoot(), LibRainDeploySnapshot.deployTag(vm), sContractName);
}

/// Where `regenerateLibs` writes. Directly under the root, so the record
/// walk — which reads tag directories — never sees it.
/// @return The lib marker path.
function libsPath() public view returns (string memory) {
return string.concat(recordRoot(), "/libs.sol");
}

/// A fixture file's content.
/// @param body What distinguishes this marker from the others.
/// @return The marker.
function marker(string memory body) public pure returns (string memory) {
// Split so `reuse lint` reads this as a fixture rather than as this
// file's own license declaration.
return string.concat("// SPDX-License", "-Identifier: LicenseRef-DCL-1.0\n// ", body, "\n");
}

/// What `regenerateSnapshots` writes over whatever was there.
/// @return The regenerated rolling snapshot.
function regeneratedSnapshot() public pure returns (string memory) {
return marker("regenerated");
}

/// What `regenerateLibs` writes: the record it could see, and whether the
/// rolling snapshot had been regenerated, at the moment it ran.
/// @param frozenCount Frozen record files visible to it.
/// @param rollingExists Whether the rolling snapshot existed.
/// @return The lib marker.
function libsMarker(uint256 frozenCount, bool rollingExists) public pure returns (string memory) {
return
marker(
string.concat("frozen ", vm.toString(frozenCount), " rolling ", rollingExists ? "present" : "absent")
);
}

/// @inheritdoc BuildScript
function snapshotContractNames() internal view override returns (string[] memory) {
string[] memory contractNames = new string[](1);
contractNames[0] = sContractName;
return contractNames;
}

/// @inheritdoc BuildScript
function regenerateSnapshots() internal override {
writeFixture(rollingPath(), regeneratedSnapshot());
}

/// @inheritdoc BuildScript
function regenerateLibs() internal override {
writeFixture(
libsPath(),
libsMarker(LibRainDeploySnapshot.frozenSnapshotPaths(vm, recordRoot()).length, vm.exists(rollingPath()))
);
}

/// Writes a marker, creating the directories above it.
/// @param path The file to write.
/// @param content The marker to write there.
function writeFixture(string memory path, string memory content) internal {
string[] memory components = vm.split(path, "/");
string memory dir = components[0];
for (uint256 i = 1; i < components.length - 1; i++) {
dir = string.concat(dir, "/", components[i]);
}
//forge-lint: disable-next-line(unsafe-cheatcode)
vm.createDir(dir, true);
//forge-lint: disable-next-line(unsafe-cheatcode)
vm.writeFile(path, content);
}
}
Loading
Loading