From 45acadeed4d149b9713a17d6cfa72e26547ee595 Mon Sep 17 00:00:00 2001 From: tequ Date: Sun, 19 Jul 2026 19:18:44 +0900 Subject: [PATCH 1/2] Initialize C projects with Hook headers Download the required Hook headers into ontracts/include during C project initialization and configure generated builds to use them. Ensure nested header files are passed as headers without being compiled as C sources, and validate incomplete API responses. --- src/c2wasm/index.ts | 2 +- src/commands.ts | 64 ++++++++++++++++++++++++++++----- src/init/c/package.json | 2 +- test/integration/c2wasm.test.ts | 49 +++++++++++++++++++++++++ test/integration/init.test.ts | 61 +++++++++++++++++++++++++++++-- 5 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 test/integration/c2wasm.test.ts 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 }); From c3b97a79867b7329283fc5bed2f95fa4d1670ad4 Mon Sep 17 00:00:00 2001 From: tequ Date: Thu, 6 Aug 2026 10:57:23 +0900 Subject: [PATCH 2/2] Derive C header file list from compile server response --- .gitignore | 4 ++-- src/commands.ts | 13 +++++-------- test/integration/init.test.ts | 1 + 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 59eb796..47d7231 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ .DS_Store # dependencies /node_modules -/dist +dist /build # .env @@ -10,4 +10,4 @@ hooks-toolkit-cli-linux hooks-toolkit-cli-macos hooks-toolkit-cli-win.exe -myproj/ \ No newline at end of file +myproj/ diff --git a/src/commands.ts b/src/commands.ts index e6fefe7..68a2192 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -26,14 +26,6 @@ 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` ); @@ -42,6 +34,11 @@ const downloadCHeaderFiles = async ( throw Error("Invalid header files response from the compile server"); } + const headerNames = Object.keys(headerFiles); + if (headerNames.length === 0) { + throw Error("No header files returned from the compile server"); + } + const files = headerNames.map((name) => { const content = headerFiles[name]; if (typeof content !== "string" || content.length === 0) { diff --git a/test/integration/init.test.ts b/test/integration/init.test.ts index ad10dd8..20cb4f0 100644 --- a/test/integration/init.test.ts +++ b/test/integration/init.test.ts @@ -184,6 +184,7 @@ describe("Init Tests", () => { hookapi: "hookapi", macro: "macro", sfcodes: "sfcodes", + tts: "", }, });