diff --git a/packages/cli/src/commands/system-types.ts b/packages/cli/src/commands/system-types.ts index 4080bf70a5..9d33068fe0 100644 --- a/packages/cli/src/commands/system-types.ts +++ b/packages/cli/src/commands/system-types.ts @@ -2,8 +2,8 @@ import type { Arguments, CommandBuilder } from "yargs"; import { generateSystemTypes } from "../utils"; type Options = { - inputDir: string; outputDir: string; + config: string; }; export const command = "system-types"; @@ -12,19 +12,15 @@ export const desc = export const builder: CommandBuilder = (yargs) => yargs.options({ - inputDir: { - type: "string", - description: "source systems directory, defaults to ./src/systems", - default: "./src/systems", - }, outputDir: { type: "string", description: "generated types directory, defaults to ./types", default: "./types", }, + config: { type: "string", default: "./deploy.json", desc: "Component and system deployment configuration" }, }); export const handler = async (args: Arguments): Promise => { - const { inputDir, outputDir } = args; - await generateSystemTypes(inputDir, outputDir); + const { outputDir, config } = args; + await generateSystemTypes(outputDir, config); }; diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 62489dd5d9..e37bb0b754 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -1,9 +1,9 @@ import type { Arguments, CommandBuilder } from "yargs"; import { execLog, generateLibDeploy, resetLibDeploy } from "../utils"; +import { getForgeConfig } from "../utils/config"; type Options = { forgeOpts?: string; - testDir: string; config: string; v: number; }; @@ -14,13 +14,16 @@ export const desc = "Run contract tests"; export const builder: CommandBuilder = (yargs) => yargs.options({ forgeOpts: { type: "string", desc: "Options passed to `forge test` command" }, - testDir: { type: "string", default: "./src/test", desc: "Test directory" }, config: { type: "string", default: "./deploy.json", desc: "Component and system deployment configuration" }, v: { type: "number", default: 2, desc: "Verbosity for forge test" }, }); export const handler = async (argv: Arguments): Promise => { - const { forgeOpts, testDir, config, v } = argv; + const { forgeOpts, config, v } = argv; + + // Get testDir from forge config + const forgeConfig = await getForgeConfig(); + const testDir = forgeConfig.test; // Generate LibDeploy.sol console.log("Generate LibDeploy.sol"); diff --git a/packages/cli/src/commands/types.ts b/packages/cli/src/commands/types.ts index 2a025b599d..569d43533e 100644 --- a/packages/cli/src/commands/types.ts +++ b/packages/cli/src/commands/types.ts @@ -1,10 +1,10 @@ -import path from "path"; import { Arguments, CommandBuilder } from "yargs"; -import { filterAbi, forgeBuild, generateAbiTypes, generateSystemTypes, generateTypes } from "../utils"; +import { generateTypes } from "../utils"; type Options = { abiDir?: string; outputDir: string; + config: string; }; export const command = "types"; @@ -21,9 +21,10 @@ export const builder: CommandBuilder = (yargs) => description: "Output directory for generated types. Defaults to ./types", default: "./types", }, + config: { type: "string", default: "./deploy.json", desc: "Component and system deployment configuration" }, }); export const handler = async (args: Arguments): Promise => { - const { abiDir, outputDir } = args; - await generateTypes(abiDir, outputDir, { clear: true }); + const { abiDir, outputDir, config } = args; + await generateTypes(config, abiDir, outputDir, { clear: true }); }; diff --git a/packages/cli/src/contracts/Deploy.sol b/packages/cli/src/contracts/Deploy.sol index 2537acff5e..bf6ac545bd 100644 --- a/packages/cli/src/contracts/Deploy.sol +++ b/packages/cli/src/contracts/Deploy.sol @@ -1,17 +1,29 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; +// NOTE: This file is autogenerated via `mud deploy-contracts` and `mud deploy`. Do not edit manually. + +// MUD +import {IDeploy} from "std-contracts/test/MudTest.t.sol"; +import {IWorld} from "solecs/interfaces/IWorld.sol"; + // Foundry import {DSTest} from "ds-test/test.sol"; import {Vm} from "forge-std/Vm.sol"; import {console} from "forge-std/console.sol"; -import {Cheats} from "./Cheats.sol"; // Libraries import {LibDeploy, DeployResult} from "./LibDeploy.sol"; -contract Deploy is DSTest { - Cheats internal immutable vm = Cheats(HEVM_ADDRESS); +contract Deploy is DSTest, IDeploy { + Vm internal immutable vm = Vm(HEVM_ADDRESS); + + function deploy(address deployer) external returns (IWorld world) { + vm.startPrank(deployer); + DeployResult memory result = LibDeploy.deploy(deployer, address(0), false); + world = result.world; + vm.stopPrank(); + } function broadcastDeploy( address _deployer, diff --git a/packages/cli/src/contracts/LibDeploy.ejs b/packages/cli/src/contracts/LibDeploy.ejs index 4b9886feab..a0ebdafaf8 100644 --- a/packages/cli/src/contracts/LibDeploy.ejs +++ b/packages/cli/src/contracts/LibDeploy.ejs @@ -2,6 +2,8 @@ pragma solidity >=0.8.0; // NOTE: This file is autogenerated via `mud codegen-libdeploy` from `deploy.json`. Do not edit manually. +// NOTE: It uses relative imports. Do not move this file, instead do `mud codegen-libdeploy --out new/path`. +// (`src` for relative paths is taken from the output of `forge config`, which in turn uses `foundry.toml`) // Foundry import { DSTest } from "ds-test/test.sol"; @@ -15,20 +17,20 @@ import { getAddressById } from "solecs/utils.sol"; import { IUint256Component } from "solecs/interfaces/IUint256Component.sol"; import { ISystem } from "solecs/interfaces/ISystem.sol"; -// Components (requires 'components=...' remapping in project's remappings.txt) +// Components <% components.forEach(component => { -%> -import { <%= component %>, ID as <%= component %>ID } from "components/<%- component %>.sol"; +import { <%= component %>, ID as <%= component %>ID } from "<%- nameToPath[component] %>"; <% }); -%> -// Systems (requires 'systems=...' remapping in project's remappings.txt) +// Systems <% systems.forEach(system => { -%> -import { <%= system.name %>, ID as <%= system.name %>ID } from "systems/<%- system.name %>.sol"; +import { <%= system.name %>, ID as <%= system.name %>ID } from "<%- nameToPath[system.name] %>"; <% }); -%> <% if (initializers.length > 0) { -%> -// Initializer libraries (requires 'libraries=...' remapping in project's remappings.txt) +// Initializer libraries <% initializers.forEach((initializer) => { -%> -import { <%= initializer %> } from "libraries/<%- initializer %>.sol"; +import { <%= initializer %> } from "<%- nameToPath[initializer] %>"; <% }); -%> <% } -%> diff --git a/packages/cli/src/utils/build.ts b/packages/cli/src/utils/build.ts index 962d6c1a09..1c27521f2f 100644 --- a/packages/cli/src/utils/build.ts +++ b/packages/cli/src/utils/build.ts @@ -1,13 +1,15 @@ import { copyFileSync, mkdirSync, readdirSync, rmSync } from "fs"; import path from "path"; +import { getForgeConfig } from "./config"; import { execLog } from "./exec"; -export function forgeBuild(out = "out", options?: { clear?: boolean }) { +export async function forgeBuild(options?: { clear?: boolean }) { if (options?.clear) { - console.log("Clearing forge build output directory", out); - rmSync(out, { recursive: true, force: true }); + const forgeConfig = await getForgeConfig(); + console.log("Clearing forge build output directory", forgeConfig.out); + rmSync(forgeConfig.out, { recursive: true, force: true }); } - return execLog("forge", ["build", "-o", out]); + return execLog("forge", ["build"]); } function getContractsInDir(dir: string, exclude?: string[]) { @@ -25,9 +27,13 @@ function copyAbi(inDir: string, outDir: string, contract: string) { } } -export function filterAbi(abiIn = "./out", abiOut = "./abi", exclude: string[] = ["Component", "IComponent"]) { - // Clean our dir - console.log(`Cleaning output directory (${abiOut}})`); +export async function filterAbi(abiOut = "./abi", exclude: string[] = ["Component", "IComponent"]) { + // Get forge output dir + const forgeConfig = await getForgeConfig(); + const abiIn = forgeConfig.out; + + // Clean abi dir + console.log(`Cleaning ABI output directory (${abiOut})`); rmSync(abiOut, { recursive: true, force: true }); mkdirSync(abiOut); diff --git a/packages/cli/src/utils/codegen.ts b/packages/cli/src/utils/codegen.ts index 403cd78bc9..455a63aa17 100644 --- a/packages/cli/src/utils/codegen.ts +++ b/packages/cli/src/utils/codegen.ts @@ -1,9 +1,12 @@ -import { readFile, writeFile } from "fs/promises"; +import { readFile, writeFile, mkdir } from "fs/promises"; import ejs from "ejs"; import path from "path"; +import { getForgeConfig } from "./config"; +import { glob } from "glob"; const contractsDir = path.join(__dirname, "../../src/contracts"); +const deployScript = "Deploy.sol"; const stubLibDeploy = readFile(path.join(contractsDir, "LibDeployStub.sol")); /** @@ -20,6 +23,13 @@ export async function generateLibDeploy(configPath: string, out: string, systems // Initializers are optional config.initializers ??= []; + // Get file paths for all the names of components, systems and initializers + // (allNames filter just helps avoid spam in logs, unused mappings wouldn't break anything) + const allNames = config.components + .concat(config.initializers) + .concat(config.systems.map(({ name }: { name: string }) => name)); + config.nameToPath = await getNameToPath(out, allNames); + // Filter systems if (systems) { const systemsArray = Array.isArray(systems) ? systems : [systems]; @@ -32,6 +42,7 @@ export async function generateLibDeploy(configPath: string, out: string, systems console.log("Generating deployment script"); const LibDeploy = await ejs.renderFile(path.join(contractsDir, "LibDeploy.ejs"), config, { async: true }); const libDeployPath = path.join(out, "LibDeploy.sol"); + await mkdir(out, { recursive: true }); await writeFile(libDeployPath, LibDeploy); return libDeployPath; @@ -40,3 +51,38 @@ export async function generateLibDeploy(configPath: string, out: string, systems export async function resetLibDeploy(out: string) { await writeFile(path.join(out, "LibDeploy.sol"), await stubLibDeploy); } + +/** + * Map ecs names to their file paths (relative to `out`) + */ +async function getNameToPath(out: string, names: string[]) { + // Use forge's config for src dir + const forgeConfig = await getForgeConfig(); + const srcDir = forgeConfig.src; + + // Recursively get all solidity files + const ecsFiles = glob.sync(path.join(srcDir, "**/*.sol")); + // And map basenames (without extension) to their paths + const nameToPath: { [key: string]: string } = {}; + for (const file of ecsFiles) { + const name = path.basename(file, ".sol"); + // skip if `name` isn't in `names` + if (names.includes(name)) { + // "./" must be added because path stripts it, + // but solidity expects it unless there's "../" ("./../" is fine) + nameToPath[name] = "./" + path.relative(out, file); + } + } + return nameToPath; +} + +/** + * Generate Deploy.sol + * @param out output directory for Deploy.sol + * @returns path to generated Deploy.sol + */ +export async function generateDeployScript(out: string) { + const deployScriptPath = path.join(out, deployScript); + await writeFile(deployScriptPath, await readFile(path.join(contractsDir, deployScript))); + return deployScriptPath; +} diff --git a/packages/cli/src/utils/config.ts b/packages/cli/src/utils/config.ts new file mode 100644 index 0000000000..08d8d736aa --- /dev/null +++ b/packages/cli/src/utils/config.ts @@ -0,0 +1,21 @@ +import { execa } from "execa"; + +export interface ForgeConfig { + // project + src: string; + test: string; + out: string; + libs: string[]; + + // all unspecified keys (this interface is far from comprehensive) + [key: string]: any; +} + +/** + * Get forge config as a parsed json object. + */ +export async function getForgeConfig() { + const { stdout } = await execa("forge", ["config", "--json"], { stdio: ["inherit", "pipe", "pipe"] }); + + return JSON.parse(stdout) as ForgeConfig; +} diff --git a/packages/cli/src/utils/deploy.ts b/packages/cli/src/utils/deploy.ts index 8bb969fc86..1717d527b3 100644 --- a/packages/cli/src/utils/deploy.ts +++ b/packages/cli/src/utils/deploy.ts @@ -1,14 +1,14 @@ import { constants, Wallet } from "ethers"; -import { generateLibDeploy, resetLibDeploy } from "./codegen"; +import { generateDeployScript, generateLibDeploy, resetLibDeploy } from "./codegen"; import { findLog } from "./findLog"; import { generateTypes } from "./types"; import { execa } from "execa"; import { StaticJsonRpcProvider } from "@ethersproject/providers"; - -const contractsDir = __dirname + "/../../src/contracts"; +import { getForgeConfig } from "./config"; /** * Deploy world, components and systems from deploy.json + * @param scriptPath path to the deploy script * @param deployerPrivateKey private key of deployer * @param rpc rpc url * @param worldAddress optional, address of existing world @@ -16,6 +16,7 @@ const contractsDir = __dirname + "/../../src/contracts"; * @returns address of deployed world */ export async function deploy( + scriptPath: string, deployerPrivateKey?: string, rpc = "http://localhost:8545", worldAddress?: string, @@ -39,7 +40,7 @@ export async function deploy( "forge", [ "script", - contractsDir + "/Deploy.sol", + scriptPath, "--target-contract", "Deploy", "-vvv", @@ -83,15 +84,23 @@ export async function generateAndDeploy(args: DeployOptions) { let deployedWorldAddress: string | undefined; let initialBlockNumber: string | undefined; + // Get testDir from forge config + const forgeConfig = await getForgeConfig(); + const testDir = forgeConfig.test; + try { + // Generate deploy script + const deployScriptPath = await generateDeployScript(testDir); + // Generate LibDeploy - libDeployPath = await generateLibDeploy(args.config, contractsDir, args.systems); + libDeployPath = await generateLibDeploy(args.config, testDir, args.systems); // Build and generate fresh types - await generateTypes(undefined, "./types", { clear: args.clear }); + await generateTypes(args.config, undefined, "./types", { clear: args.clear }); // Call deploy script const result = await deploy( + deployScriptPath, args.deployerPrivateKey, args.rpc, args.worldAddress, @@ -100,14 +109,12 @@ export async function generateAndDeploy(args: DeployOptions) { ); deployedWorldAddress = result.deployedWorldAddress; initialBlockNumber = result.initialBlockNumber; - - // Extract world address from deploy script } catch (e) { console.error(e); } finally { // Remove generated LibDeploy console.log("Cleaning up deployment script"); - if (libDeployPath) await resetLibDeploy(contractsDir); + if (libDeployPath) await resetLibDeploy(testDir); } return { deployedWorldAddress, initialBlockNumber }; diff --git a/packages/cli/src/utils/types.ts b/packages/cli/src/utils/types.ts index dac1001ce1..035ac1f98a 100644 --- a/packages/cli/src/utils/types.ts +++ b/packages/cli/src/utils/types.ts @@ -1,10 +1,10 @@ import { runTypeChain, glob as typechainGlob } from "typechain"; -import { deferred } from "./deferred"; import glob from "glob"; import { extractIdFromFile } from "./ids"; -import { rmSync, writeFileSync } from "fs"; +import { readFileSync, rmSync, writeFileSync } from "fs"; import path from "path"; import { filterAbi, forgeBuild } from "./build"; +import { getForgeConfig } from "./config"; export async function generateAbiTypes( inputDir: string, @@ -31,81 +31,81 @@ export async function generateAbiTypes( console.log(`Successfully generated ${result.filesGenerated} files`); } -export async function generateSystemTypes(inputDir: string, outputDir: string, options?: { clear?: boolean }) { - if (options?.clear) { - console.log("Clearing system type output files", outputDir); - rmSync(path.join(outputDir, "/SystemTypes.ts"), { force: true }); - rmSync(path.join(outputDir, "/SystemAbis.mts"), { force: true }); - rmSync(path.join(outputDir, "/SystemAbis.mjs"), { force: true }); - rmSync(path.join(outputDir, "/SystemMappings.ts"), { force: true }); - } - - let abis: string[] = []; - let systems: string[] = []; - let ids: string[] = []; - let typePaths: string[] = []; - - const systemsPath = `${inputDir}/*.sol`; - - const [resolve, , promise] = deferred(); - glob(systemsPath, {}, (_, matches) => { - systems = matches.map((path) => { - const fragments = path.split("/"); - return fragments[fragments.length - 1].split(".sol")[0]; - }); - - ids = matches.map((path, index) => { - const id = extractIdFromFile(path); - if (!id) { - console.log("Path:", path); - console.log("ID:", id); - throw new Error( - "No ID found for" + - matches[index] + - ". Make sure your system source file includes a ID definition (uint256 constant ID = uint256(keccak256());)" - ); - } - return id; - }); - - abis = systems.map((system) => `../abi/${system}.json`); - - typePaths = systems.map((system) => `./ethers-contracts/${system}.ts`); - - resolve(); +export async function generateSystemTypes(outputDir: string, configPath: string) { + // Parse deploy.json config (to get all the systems) + const deployConfig = JSON.parse(readFileSync(configPath, { encoding: "utf8" })); + const systemNames: string[] = deployConfig.systems.map(({ name }: { name: string }) => name); + + // Parse forge config (to get src dir) + const forgeConfig = await getForgeConfig(); + + // Get all .sol files + const systemFiles = glob + .sync(path.join(forgeConfig.src, "**/*.sol")) + // Keep only files that match system names + .filter((file) => systemNames.includes(path.basename(file, ".sol"))); + + // Combine systems data + const systems = systemFiles.map((file) => { + // Extract ids from system files + const id = extractIdFromFile(file); + if (!id) { + console.log("Path:", file); + console.log("ID:", id); + throw new Error( + `No ID found for ${file}. Make sure your system source file includes a ID definition (uint256 constant ID = uint256(keccak256());)` + ); + } + + const name = path.basename(file, ".sol"); + return { + name, + id, + abi: `../abi/${name}.json`, + typePath: `./ethers-contracts/${name}.ts`, + }; }); - // Make the callback synchronous - await promise; - - console.log("Matches", systems); - console.log("Solidity", ids); - console.log("Type paths", typePaths); - console.log("ABIs", abis); + console.log( + "System names", + systems.map(({ name }) => name) + ); + console.log( + "System ids", + systems.map(({ id }) => id) + ); + console.log( + "Type paths", + systems.map(({ typePath }) => typePath) + ); + console.log( + "ABIs", + systems.map(({ abi }) => abi) + ); const SystemMappings = `// Autogenerated using mud system-types export const systemToId = { -${systems.map((system, index) => ` ${system}: "${ids[index]}",`).join("\n")} +${systems.map(({ id, name }) => ` ${name}: "${id}",`).join("\n")} }; export const idToSystem = { -${ids.map((id, index) => ` "${id}": "${systems[index]}",`).join("\n")} +${systems.map(({ id, name }) => ` "${id}": "${name}",`).join("\n")} }; - `; +`; const SystemTypes = `// Autogenerated using mud system-types -${typePaths.map((path, index) => `import { ${systems[index]} } from "${path.replace(".ts", "")}";`).join("\n")} +${systems.map(({ name, typePath }) => `import { ${name} } from "${typePath.replace(".ts", "")}";`).join("\n")} export type SystemTypes = { -${systems.map((system, index) => ` "${ids[index]}": ${system};`).join("\n")} +${systems.map(({ id, name }) => ` "${id}": ${name};`).join("\n")} }; `; const SystemAbis = `// Autogenerated using mud system-types -${abis.map((path, index) => `import ${systems[index]} from "${path}";`).join("\n")} +${systems.map(({ name, abi }) => `import ${name} from "${abi}";`).join("\n")} export const SystemAbis = { -${systems.map((system, index) => ` "${ids[index]}": ${system}.abi,`).join("\n")} +${systems.map(({ id, name }) => ` "${id}": ${name}.abi,`).join("\n")} }; `; @@ -114,24 +114,28 @@ ${systems.map((system, index) => ` "${ids[index]}": ${system}.abi,`).join("\n") console.log("SystemAbis.mts", SystemAbis); console.log("SystemMappings.ts", SystemMappings); - writeFileSync(`${outputDir}/SystemTypes.ts`, SystemTypes); - writeFileSync(`${outputDir}/SystemAbis.mts`, SystemAbis); - writeFileSync(`${outputDir}/SystemAbis.mjs`, SystemAbis); - writeFileSync(`${outputDir}/SystemMappings.ts`, SystemMappings); + writeFileSync(path.join(outputDir, "SystemTypes.ts"), SystemTypes); + writeFileSync(path.join(outputDir, "SystemAbis.mts"), SystemAbis); + writeFileSync(path.join(outputDir, "SystemAbis.mjs"), SystemAbis); + writeFileSync(path.join(outputDir, "SystemMappings.ts"), SystemMappings); } /** * @param abiDir If not provided, the contracts are built and abis are exported to ./abi */ -export async function generateTypes(abiDir?: string, outputDir = "./types", options?: { clear?: boolean }) { +export async function generateTypes( + configPath: string, + abiDir?: string, + outputDir = "./types", + options?: { clear?: boolean } +) { if (!abiDir) { console.log("Compiling contracts"); - const buildOutput = "./out"; abiDir = "./abi"; - await forgeBuild(buildOutput, options); - filterAbi(buildOutput, abiDir); + await forgeBuild(options); + await filterAbi(abiDir); } await generateAbiTypes(abiDir, path.join(outputDir, "ethers-contracts"), options); - await generateSystemTypes("./src/systems", outputDir, options); + await generateSystemTypes(outputDir, configPath); }