diff --git a/src/c2wasm/index.ts b/src/c2wasm/index.ts index d74363e..ace5a0b 100644 --- a/src/c2wasm/index.ts +++ b/src/c2wasm/index.ts @@ -27,7 +27,7 @@ export async function buildCDir( // Reading all files in the directory tree let fileObjects: any[]; try { - fileObjects = readFiles(dirPath); + fileObjects = readFiles(dirPath).filter((file) => file.type === "c"); } catch (error: any) { console.error(`Error reading files: ${error}`); process.exit(1); diff --git a/src/commands.ts b/src/commands.ts index babf841..e6fefe7 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -22,6 +22,41 @@ const copyFiles = (source: string, destination: string) => { }); }; +const downloadCHeaderFiles = async ( + compileHost: string, + projectDir: string +): Promise => { + const headerNames = [ + "error", + "extern", + "hookapi", + "macro", + "sfcodes", + "tts", + ] as const; + const response = await axios.get( + `${compileHost.replace(/\/+$/, "")}/api/header-files` + ); + const headerFiles = response.data; + if (!headerFiles || typeof headerFiles !== "object") { + throw Error("Invalid header files response from the compile server"); + } + + const files = headerNames.map((name) => { + const content = headerFiles[name]; + if (typeof content !== "string" || content.length === 0) { + throw Error(`Missing or invalid header file: ${name}.h`); + } + return { filename: `${name}.h`, content }; + }); + + const includeDir = path.join(projectDir, "contracts", "include"); + fs.mkdirSync(includeDir, { recursive: true }); + files.forEach(({ filename, content }) => { + fs.writeFileSync(path.join(includeDir, filename), content, "utf-8"); + }); +}; + const clean = (filePath: string, outputPath?: string): string => { const tsCode = fs.readFileSync(filePath, "utf-8"); const importPattern = /^\s*import\s+.*?;\s*$/gm; @@ -76,15 +111,28 @@ export const initCommand = async (type: "c" | "js", folderName: string) => { type === "c" ? "CHooks" : "JSHooks" } project in ${newProjectDir}` ); - try { - const projectEnv = dotenv.config({ - path: path.join(newProjectDir, ".env"), - }); - if (!projectEnv.parsed) { - console.log("No .env file found in the project template."); - process.exit(1); + const projectEnv = dotenv.config({ + path: path.join(newProjectDir, ".env"), + }); + if (!projectEnv.parsed) { + console.log("No .env file found in the project template."); + process.exit(1); + } + const env = projectEnv.parsed; + if (type === "c") { + const compileHost = env.HOOKS_COMPILE_HOST; + if (!compileHost) { + throw Error("HOOKS_COMPILE_HOST is not set in the project template"); + } + try { + await downloadCHeaderFiles(compileHost, newProjectDir); + console.log("Header files saved to contracts/include."); + } catch (error) { + console.error("Error downloading header files:", error); + throw error; } - const env = projectEnv.parsed; + } + try { const aliceResponse = await axios.post( `https://${env.XRPLD_WSS.replace("wss://", "")}/newcreds` ); diff --git a/src/init/c/package.json b/src/init/c/package.json index b9c6e5c..37ca6b3 100644 --- a/src/init/c/package.json +++ b/src/init/c/package.json @@ -4,7 +4,7 @@ "description": "", "main": "src/index.ts", "scripts": { - "build": "hooks-cli compile-c contracts build/", + "build": "hooks-cli compile-c contracts build/ --headers contracts/include", "deploy": "yarn run build && ts-node src/index.ts" }, "author": { diff --git a/test/integration/c2wasm.test.ts b/test/integration/c2wasm.test.ts new file mode 100644 index 0000000..e4c1df6 --- /dev/null +++ b/test/integration/c2wasm.test.ts @@ -0,0 +1,49 @@ +import axios from "axios"; +import * as fs from "fs"; +import * as path from "path"; +import { buildCDir } from "../../src/c2wasm"; +import { decodeBinary } from "../../src/2bytes/decodeBinary"; + +jest.mock("axios"); +jest.mock("../../src/2bytes/decodeBinary"); + +describe("C directory builds", () => { + const projectDir = path.join(process.cwd(), "test-c-build-project"); + const contractsDir = path.join(projectDir, "contracts"); + const includeDir = path.join(contractsDir, "include"); + const outDir = path.join(projectDir, "build"); + + beforeEach(() => { + jest.clearAllMocks(); + fs.mkdirSync(includeDir, { recursive: true }); + fs.writeFileSync(path.join(contractsDir, "base.c"), "int64_t hook() {}\n"); + fs.writeFileSync(path.join(includeDir, "macro.h"), "#define TEST 1\n"); + }); + + afterEach(() => { + fs.rmSync(projectDir, { recursive: true, force: true }); + }); + + it("does not compile headers found below the source directory", async () => { + (decodeBinary as jest.Mock).mockResolvedValue(new Uint8Array([0])); + (axios.post as jest.Mock).mockResolvedValue({ + data: { + success: true, + message: "", + output: "binary", + tasks: [], + }, + }); + + await buildCDir(contractsDir, outDir, includeDir, false); + + expect(axios.post).toHaveBeenCalledTimes(1); + const body = JSON.parse((axios.post as jest.Mock).mock.calls[0][1]); + expect(body.files).toEqual([ + expect.objectContaining({ name: "base.c", type: "c" }), + ]); + expect(body.headers).toEqual([ + expect.objectContaining({ name: "macro.h", type: "h" }), + ]); + }); +}); diff --git a/test/integration/init.test.ts b/test/integration/init.test.ts index 7912262..ad10dd8 100644 --- a/test/integration/init.test.ts +++ b/test/integration/init.test.ts @@ -38,6 +38,8 @@ describe("Init Tests", () => { const tempPathJS = path.join(__dirname, "..", "..", "src", "init", "js"); beforeEach(() => { + jest.clearAllMocks(); + // Clean up any existing directories if (fs.existsSync(projectPathC)) { fs.rmSync(projectPathC, { recursive: true, force: true }); @@ -58,8 +60,16 @@ describe("Init Tests", () => { }); it("should initialize a new C project", async () => { - // Mock axios response - jest.mock("axios"); + const headerFiles = { + error: "#define INTERNAL_ERROR -2\n", + extern: "extern int64_t accept(uint32_t, uint32_t, int64_t);\n", + hookapi: '#include "macro.h"\n', + macro: "#define SBUF(str) (uint32_t)(str), sizeof(str)\n", + sfcodes: "#define sfAccount ((8U << 16U) + 1U)\n", + tts: "#define ttINVOKE 99\n", + }; + + (axios.get as jest.Mock).mockResolvedValue({ data: headerFiles }); (axios.post as jest.Mock).mockResolvedValue({ data: { code: "tesSUCCESS", @@ -97,6 +107,30 @@ describe("Init Tests", () => { expect(env?.XRPLD_WSS).toBeDefined(); expect(env?.XRPLD_WSS).toEqual(originalEnv?.XRPLD_WSS); expect(env?.ALICE_SEED).toBeDefined(); + + expect(axios.get).toHaveBeenCalledWith( + `${originalEnv?.HOOKS_COMPILE_HOST}/api/header-files` + ); + + const includePath = path.join(projectPathC, "contracts", "include"); + expect(fs.statSync(includePath).isDirectory()).toBe(true); + expect(fs.readdirSync(includePath).sort()).toEqual( + Object.keys(headerFiles) + .map((name) => `${name}.h`) + .sort() + ); + Object.entries(headerFiles).forEach(([name, content]) => { + expect( + fs.readFileSync(path.join(includePath, `${name}.h`), "utf-8") + ).toBe(content); + }); + + const generatedPackage = JSON.parse( + fs.readFileSync(path.join(projectPathC, "package.json"), "utf-8") + ); + expect(generatedPackage.scripts.build).toBe( + "hooks-cli compile-c contracts build/ --headers contracts/include" + ); }); it("should initialize a new JS project", async () => { @@ -110,6 +144,8 @@ describe("Init Tests", () => { await expect(initCommand("js", folderNameJS)).resolves.not.toThrow(); + expect(axios.get).not.toHaveBeenCalled(); + // Verify that the directory was created expect(fs.existsSync(projectPathJS)).toBe(true); @@ -140,6 +176,27 @@ describe("Init Tests", () => { expect(env?.ALICE_SEED).toBeDefined(); }); + it("should fail C initialization when a required header is missing", async () => { + (axios.get as jest.Mock).mockResolvedValue({ + data: { + error: "error", + extern: "extern", + hookapi: "hookapi", + macro: "macro", + sfcodes: "sfcodes", + }, + }); + + await expect(initCommand("c", folderNameC)).rejects.toThrow( + "Missing or invalid header file: tts.h" + ); + + expect(axios.post).not.toHaveBeenCalled(); + expect( + fs.existsSync(path.join(projectPathC, "contracts", "include")) + ).toBe(false); + }); + it("should handle existing directory error", async () => { // Create the directory beforehand to simulate existing directory fs.mkdirSync(projectPathC, { recursive: true });