Skip to content
Closed
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
12 changes: 4 additions & 8 deletions packages/cli/src/commands/system-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -12,19 +12,15 @@ export const desc =

export const builder: CommandBuilder<Options, Options> = (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<Options>): Promise<void> => {
const { inputDir, outputDir } = args;
await generateSystemTypes(inputDir, outputDir);
const { outputDir, config } = args;
await generateSystemTypes(outputDir, config);
};
9 changes: 6 additions & 3 deletions packages/cli/src/commands/test.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Expand All @@ -14,13 +14,16 @@ export const desc = "Run contract tests";
export const builder: CommandBuilder<Options, Options> = (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<Options>): Promise<void> => {
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");
Expand Down
9 changes: 5 additions & 4 deletions packages/cli/src/commands/types.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -21,9 +21,10 @@ export const builder: CommandBuilder<Options, Options> = (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<Options>): Promise<void> => {
const { abiDir, outputDir } = args;
await generateTypes(abiDir, outputDir, { clear: true });
const { abiDir, outputDir, config } = args;
await generateTypes(config, abiDir, outputDir, { clear: true });
};
18 changes: 15 additions & 3 deletions packages/cli/src/contracts/Deploy.sol
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
14 changes: 8 additions & 6 deletions packages/cli/src/contracts/LibDeploy.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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] %>";
<% }); -%>
<% } -%>

Expand Down
20 changes: 13 additions & 7 deletions packages/cli/src/utils/build.ts
Original file line number Diff line number Diff line change
@@ -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[]) {
Expand All @@ -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);

Expand Down
48 changes: 47 additions & 1 deletion packages/cli/src/utils/codegen.ts
Original file line number Diff line number Diff line change
@@ -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"));

/**
Expand All @@ -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];
Expand All @@ -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;
Expand All @@ -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;
}
21 changes: 21 additions & 0 deletions packages/cli/src/utils/config.ts
Original file line number Diff line number Diff line change
@@ -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;
}
25 changes: 16 additions & 9 deletions packages/cli/src/utils/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
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
* @param reuseComponents optional, reuse existing components
* @returns address of deployed world
*/
export async function deploy(
scriptPath: string,
deployerPrivateKey?: string,
rpc = "http://localhost:8545",
worldAddress?: string,
Expand All @@ -39,7 +40,7 @@ export async function deploy(
"forge",
[
"script",
contractsDir + "/Deploy.sol",
scriptPath,
"--target-contract",
"Deploy",
"-vvv",
Expand Down Expand Up @@ -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,
Expand All @@ -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 };
Expand Down
Loading