diff --git a/dist/action.mjs b/dist/action.mjs index 57061eb..5b8ed0e 100644 --- a/dist/action.mjs +++ b/dist/action.mjs @@ -2,7 +2,7 @@ import 'node:fs'; import fsPromises from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import { execFileSync } from 'node:child_process'; +import { spawn } from 'node:child_process'; /** * @internal @@ -52,32 +52,64 @@ function logError(err) { } /** - * Configures the build system of a CMake project. + * Executes a command with the given arguments. * - * @param context - The action context. + * The command is executed with `stdin` ignored and both `stdout` and `stderr` inherited by the parent process. + * + * @param command The command to execute. + * @param args The arguments to pass to the command. + * @returns A promise that resolves when the command exits successfully or rejects if it exits with a non-zero status code or encounters an error. */ -function configureProject(context) { +async function exec(command, args) { + return new Promise((resolve, reject) => { + const proc = spawn(command, args, { + stdio: ["ignore", "inherit", "inherit"], + }); + proc.on("error", reject); + proc.on("close", (code) => { + if (code === 0) { + resolve(); + } + else { + reject(new Error(`Command exited with status code ${code}`)); + } + }); + }); +} + +/** + * Configures the build system for a CMake project. + * + * Constructs and runs the `cmake` command to configure the project with the specified + * source directory, build directory, generator, options, and additional arguments. + * + * @param context - The action context containing configuration details. + * @returns A promise that resolves when the build system is successfully configured. + */ +async function configureProject(context) { const configureArgs = []; if (context.sourceDir) { configureArgs.push(context.sourceDir); } configureArgs.push("-B", context.buildDir); if (context.configure.generator) { - configureArgs.push(...["-G", context.configure.generator]); + configureArgs.push("-G", context.configure.generator); } configureArgs.push(...context.configure.options.map((opt) => "-D" + opt)); configureArgs.push(...context.configure.args); - execFileSync("cmake", configureArgs, { stdio: "inherit" }); + await exec("cmake", configureArgs); } /** - * Build a CMake project. + * Builds a CMake project. * - * @param context - The action context. + * Runs the `cmake --build` command to build the project using the specified + * build directory and additional arguments. + * + * @param context - The action context containing build details. + * @returns A promise that resolves when the project is successfully built. */ -function buildProject(context) { - execFileSync("cmake", ["--build", context.buildDir, ...context.build.args], { - stdio: "inherit", - }); +async function buildProject(context) { + await exec("cmake", ["--build", context.buildDir, ...context.build.args]); } var shellQuote = {}; @@ -393,10 +425,10 @@ function getContext() { try { const context = getContext(); - configureProject(context); + await configureProject(context); await setOutput("build-dir", context.buildDir); if (context.build.enabled) { - buildProject(context); + await buildProject(context); } } catch (err) { diff --git a/src/action.ts b/src/action.ts index cd1df4b..632f274 100644 --- a/src/action.ts +++ b/src/action.ts @@ -5,12 +5,12 @@ import { getContext } from "./context.js"; try { const context = getContext(); - configureProject(context); + await configureProject(context); await setOutput("build-dir", context.buildDir); if (context.build.enabled) { - buildProject(context); + await buildProject(context); } } catch (err) { logError(err); diff --git a/src/cmake.test.ts b/src/cmake.test.ts index 0b676e8..2b10dec 100644 --- a/src/cmake.test.ts +++ b/src/cmake.test.ts @@ -21,8 +21,8 @@ const defaultContext: Context = { }, }; -jest.unstable_mockModule("node:child_process", () => ({ - execFileSync: jest.fn(), +jest.unstable_mockModule("./exec.js", () => ({ + exec: jest.fn(), })); describe("configure a CMake project", () => { @@ -101,18 +101,14 @@ describe("configure a CMake project", () => { for (const testCase of testCases) { it(`should execute the correct command ${testCase.name}`, async () => { const { configureProject } = await import("./cmake.js"); - const { execFileSync } = await import("node:child_process"); + const { exec } = await import("./exec.js"); - jest.mocked(execFileSync).mockReset(); + jest.mocked(exec).mockReset(); - configureProject({ ...defaultContext, ...testCase.context }); + await configureProject({ ...defaultContext, ...testCase.context }); - expect(execFileSync).toHaveBeenCalledTimes(1); - expect(execFileSync).toHaveBeenLastCalledWith( - "cmake", - testCase.expectedArgs, - { stdio: "inherit" }, - ); + expect(exec).toHaveBeenCalledTimes(1); + expect(exec).toHaveBeenLastCalledWith("cmake", testCase.expectedArgs); }); } }); @@ -149,18 +145,14 @@ describe("build a CMake project", () => { for (const testCase of testCases) { it(`should execute the correct command ${testCase.name}`, async () => { const { buildProject } = await import("./cmake.js"); - const { execFileSync } = await import("node:child_process"); + const { exec } = await import("./exec.js"); - jest.mocked(execFileSync).mockReset(); + jest.mocked(exec).mockReset(); - buildProject({ ...defaultContext, ...testCase.context }); + await buildProject({ ...defaultContext, ...testCase.context }); - expect(execFileSync).toHaveBeenCalledTimes(1); - expect(execFileSync).toHaveBeenLastCalledWith( - "cmake", - testCase.expectedArgs, - { stdio: "inherit" }, - ); + expect(exec).toHaveBeenCalledTimes(1); + expect(exec).toHaveBeenLastCalledWith("cmake", testCase.expectedArgs); }); } }); diff --git a/src/cmake.ts b/src/cmake.ts index 512aa6b..3c97234 100644 --- a/src/cmake.ts +++ b/src/cmake.ts @@ -1,12 +1,16 @@ -import { execFileSync } from "node:child_process"; +import { exec } from "./exec.js"; import type { Context } from "./context.js"; /** - * Configures the build system of a CMake project. + * Configures the build system for a CMake project. * - * @param context - The action context. + * Constructs and runs the `cmake` command to configure the project with the specified + * source directory, build directory, generator, options, and additional arguments. + * + * @param context - The action context containing configuration details. + * @returns A promise that resolves when the build system is successfully configured. */ -export function configureProject(context: Context): void { +export async function configureProject(context: Context): Promise { const configureArgs = []; if (context.sourceDir) { @@ -16,22 +20,24 @@ export function configureProject(context: Context): void { configureArgs.push("-B", context.buildDir); if (context.configure.generator) { - configureArgs.push(...["-G", context.configure.generator]); + configureArgs.push("-G", context.configure.generator); } configureArgs.push(...context.configure.options.map((opt) => "-D" + opt)); configureArgs.push(...context.configure.args); - execFileSync("cmake", configureArgs, { stdio: "inherit" }); + await exec("cmake", configureArgs); } /** - * Build a CMake project. + * Builds a CMake project. * - * @param context - The action context. + * Runs the `cmake --build` command to build the project using the specified + * build directory and additional arguments. + * + * @param context - The action context containing build details. + * @returns A promise that resolves when the project is successfully built. */ -export function buildProject(context: Context): void { - execFileSync("cmake", ["--build", context.buildDir, ...context.build.args], { - stdio: "inherit", - }); +export async function buildProject(context: Context): Promise { + await exec("cmake", ["--build", context.buildDir, ...context.build.args]); } diff --git a/src/exec.test.ts b/src/exec.test.ts new file mode 100644 index 0000000..0aca427 --- /dev/null +++ b/src/exec.test.ts @@ -0,0 +1,13 @@ +import { exec } from "./exec.js"; + +describe("execute commands", () => { + it("should successfully execute a command", async () => { + await exec("node", ["--version"]); + }); + + it("should fail to execute a command", async () => { + await expect(exec("node", ["--invalid"])).rejects.toThrow( + "Command exited with status code 9", + ); + }); +}); diff --git a/src/exec.ts b/src/exec.ts new file mode 100644 index 0000000..4d5f222 --- /dev/null +++ b/src/exec.ts @@ -0,0 +1,26 @@ +import { spawn } from "node:child_process"; + +/** + * Executes a command with the given arguments. + * + * The command is executed with `stdin` ignored and both `stdout` and `stderr` inherited by the parent process. + * + * @param command The command to execute. + * @param args The arguments to pass to the command. + * @returns A promise that resolves when the command exits successfully or rejects if it exits with a non-zero status code or encounters an error. + */ +export async function exec(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const proc = spawn(command, args, { + stdio: ["ignore", "inherit", "inherit"], + }); + proc.on("error", reject); + proc.on("close", (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Command exited with status code ${code}`)); + } + }); + }); +}