Compare commits

..

No commits in common. "main" and "v2.0.0" have entirely different histories.
main ... v2.0.0

38 changed files with 33159 additions and 3349 deletions

1
.env.yarn Normal file
View File

@ -0,0 +1 @@
NODE_OPTIONS=--experimental-vm-modules

2
.gitattributes vendored
View File

@ -1,2 +1,2 @@
dist/** -diff linguist-generated
pnpm-lock.yaml -diff linguist-generated
yarn.lock -diff linguist-generated

View File

@ -6,7 +6,7 @@ updates:
interval: daily
commit-message:
prefix: chore
labels: []
labels: [chore]
- package-ecosystem: npm
directory: /
@ -14,5 +14,5 @@ updates:
interval: daily
commit-message:
prefix: chore
labels: []
labels: [chore]
versioning-strategy: increase

View File

@ -5,30 +5,24 @@ on:
push:
branches: [main]
jobs:
build-action:
name: Build Action
runs-on: ubuntu-24.04
build-package:
name: Build Package
runs-on: ubuntu-latest
steps:
- name: Checkout Project
uses: actions/checkout@v5.0.0
uses: actions/checkout@v4.1.7
- name: Setup pnpm
uses: threeal/setup-pnpm-action@v1.0.0
- name: Setup Node.js
uses: actions/setup-node@v4.0.3
with:
node-version: latest
- name: Install Dependencies
run: pnpm install
- name: Setup Yarn
uses: threeal/setup-yarn-action@v2.0.0
with:
version: stable
- name: Check Types
run: pnpm tsc
- name: Test Action
run: pnpm test
- name: Check Formatting
run: pnpm prettier --check .
- name: Check Lint
run: pnpm eslint
- name: Build Action
run: pnpm rollup -c && git diff --exit-code dist
- name: Build Package
run: |
yarn build
git diff --exit-code HEAD

31
.github/workflows/check.yaml vendored Normal file
View File

@ -0,0 +1,31 @@
name: Check
on:
workflow_dispatch:
pull_request:
push:
branches: [main]
jobs:
check-package:
name: Check Package
runs-on: ubuntu-latest
steps:
- name: Checkout Project
uses: actions/checkout@v4.1.7
- name: Setup Node.js
uses: actions/setup-node@v4.0.3
with:
node-version: latest
- name: Setup Yarn
uses: threeal/setup-yarn-action@v2.0.0
with:
version: stable
- name: Check Formatting
run: |
yarn format
git diff --exit-code HEAD
- name: Check Lint
run: yarn lint

View File

@ -5,22 +5,42 @@ on:
push:
branches: [main]
jobs:
test-package:
name: Test Package
runs-on: ubuntu-latest
steps:
- name: Checkout Project
uses: actions/checkout@v4.1.7
- name: Setup Node.js
uses: actions/setup-node@v4.0.3
with:
node-version: latest
- name: Setup Yarn
uses: threeal/setup-yarn-action@v2.0.0
with:
version: stable
- name: Test Package
run: yarn test
test-action:
name: Test Action
runs-on: ${{ matrix.os }}
runs-on: ${{ matrix.os }}-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-14, windows-2022]
os: [windows, ubuntu, macos]
steps:
- name: Checkout Sample Project
uses: actions/checkout@v5.0.0
uses: actions/checkout@v4.1.7
with:
repository: threeal/cpp-starter
ref: v1.0.0
- name: Checkout Action
uses: actions/checkout@v5.0.0
uses: actions/checkout@v4.1.7
with:
path: cmake-action
sparse-checkout: |
@ -33,21 +53,21 @@ jobs:
uses: ./cmake-action
- name: Run Sample Project
run: ${{ steps.cmake-action.outputs.build-dir }}/${{ matrix.os == 'windows-2022' && 'Debug/generate_sequence.exe' || 'generate_sequence' }} 5
run: ${{ steps.cmake-action.outputs.build-dir }}/${{ matrix.os == 'windows' && 'Debug/generate_sequence.exe' || 'generate_sequence' }} 5
test-action-with-specified-dirs:
name: Test Action With Specified Directories
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- name: Checkout Sample Project
uses: actions/checkout@v5.0.0
uses: actions/checkout@v4.1.7
with:
repository: threeal/cpp-starter
ref: v1.0.0
path: project
- name: Checkout Action
uses: actions/checkout@v5.0.0
uses: actions/checkout@v4.1.7
with:
path: cmake-action
sparse-checkout: |
@ -67,16 +87,16 @@ jobs:
test-action-without-run-build:
name: Test Action Without Run Build
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- name: Checkout Sample Project
uses: actions/checkout@v5.0.0
uses: actions/checkout@v4.1.7
with:
repository: threeal/cpp-starter
ref: v1.0.0
- name: Checkout Action
uses: actions/checkout@v5.0.0
uses: actions/checkout@v4.1.7
with:
path: cmake-action
sparse-checkout: |
@ -94,16 +114,16 @@ jobs:
test-action-with-additional-options:
name: Test Action With Additional Options
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- name: Checkout Sample Project
uses: actions/checkout@v5.0.0
uses: actions/checkout@v4.1.7
with:
repository: threeal/cpp-starter
ref: v1.0.0
- name: Checkout Action
uses: actions/checkout@v5.0.0
uses: actions/checkout@v4.1.7
with:
path: cmake-action
sparse-checkout: |
@ -121,16 +141,16 @@ jobs:
test-action-with-custom-generator:
name: Test Action With Custom Generator
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- name: Checkout Sample Project
uses: actions/checkout@v5.0.0
uses: actions/checkout@v4.1.7
with:
repository: threeal/cpp-starter
ref: v1.0.0
- name: Checkout Action
uses: actions/checkout@v5.0.0
uses: actions/checkout@v4.1.7
with:
path: cmake-action
sparse-checkout: |
@ -138,6 +158,9 @@ jobs:
dist
sparse-checkout-cone-mode: false
- name: Setup Ninja
uses: seanmiddleditch/gha-setup-ninja@v5
- name: Configure Sample Project
id: cmake-action
uses: ./cmake-action

3
.gitignore vendored
View File

@ -1,6 +1,5 @@
.*
!.env.yarn
!.git*
!.npmrc
!.prettier*
node_modules/

1
.npmrc
View File

@ -1 +0,0 @@
use-node-version=24.7.0

View File

@ -1,3 +0,0 @@
dist
pnpm-lock.yaml
README.md

View File

@ -1,3 +0,0 @@
{
"plugins": ["prettier-plugin-organize-imports"]
}

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023-2025 Alfi Maulana
Copyright (c) 2023-2024 Alfi Maulana
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -37,13 +37,13 @@ on:
jobs:
build-project:
name: Build Project
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- name: Checkout Project
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.7
- name: Build Project
uses: threeal/cmake-action@v2.1.0
uses: threeal/cmake-action@v2.0.0
```
### Specify the Source and Build Directories
@ -52,7 +52,7 @@ By default, this action uses the current working directory as the source directo
```yaml
- name: Build Project
uses: threeal/cmake-action@v2.1.0
uses: threeal/cmake-action@v2.0.0
with:
source-dir: source
build-dir: output
@ -63,8 +63,11 @@ By default, this action uses the current working directory as the source directo
The following example demonstrates how to use this action to configure and build the project using [Ninja](https://ninja-build.org/) as the build system generator and [Clang](https://clang.llvm.org/) as the compiler:
```yaml
- name: Setup Ninja
uses: seanmiddleditch/gha-setup-ninja@v5
- name: Build Project
uses: threeal/cmake-action@v2.1.0
uses: threeal/cmake-action@v2.0.0
with:
generator: Ninja
cxx-compiler: clang++
@ -76,7 +79,7 @@ Use the `options` input to specify additional options for configuring a project:
```yaml
- name: Build Project
uses: threeal/cmake-action@v2.1.0
uses: threeal/cmake-action@v2.0.0
with:
options: |
BUILD_TESTS=ON
@ -91,7 +94,7 @@ By default, this action builds the project after configuration. To skip the buil
```yaml
- name: Configure Project
uses: threeal/cmake-action@v2.1.0
uses: threeal/cmake-action@v2.0.0
with:
run-build: false
```
@ -100,4 +103,4 @@ By default, this action builds the project after configuration. To skip the buil
This project is licensed under the terms of the [MIT License](./LICENSE).
Copyright © 2023-2025 [Alfi Maulana](https://github.com/threeal/)
Copyright © 2023-2024 [Alfi Maulana](https://github.com/threeal/)

View File

@ -33,5 +33,5 @@ outputs:
description: The build directory of the CMake project
value: ${{ steps.process-inputs.outputs.build-dir }}
runs:
using: node24
main: dist/action.mjs
using: node20
main: dist/index.js

193
dist/action.mjs generated vendored
View File

@ -1,193 +0,0 @@
import 'node:fs';
import fsPromises from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
/**
* @internal
* Retrieves the value of an environment variable.
*
* @param name - The name of the environment variable.
* @returns The value of the environment variable.
* @throws Error if the environment variable is not defined.
*/
function mustGetEnvironment(name) {
const value = process.env[name];
if (value === undefined) {
throw new Error(`the ${name} environment variable must be defined`);
}
return value;
}
/**
* Retrieves the value of a GitHub Actions input.
*
* @param name - The name of the GitHub Actions input.
* @returns The value of the GitHub Actions input, or an empty string if not found.
*/
function getInput(name) {
const value = process.env[`INPUT_${name.toUpperCase()}`] ?? "";
return value.trim();
}
/**
* Sets the value of a GitHub Actions output.
*
* @param name - The name of the GitHub Actions output.
* @param value - The value to set for the GitHub Actions output.
* @returns A promise that resolves when the value is successfully set.
*/
async function setOutput(name, value) {
const filePath = mustGetEnvironment("GITHUB_OUTPUT");
await fsPromises.appendFile(filePath, `${name}=${value}${os.EOL}`);
}
/**
* Logs an error message in GitHub Actions.
*
* @param err - The error, which can be of any type.
*/
function logError(err) {
const message = err instanceof Error ? err.message : String(err);
process.stdout.write(`::error::${message}${os.EOL}`);
}
/**
* Logs a command along with its arguments in GitHub Actions.
*
* @param command - The command to log.
* @param args - The arguments of the command.
*/
function logCommand(command, ...args) {
const message = [command, ...args].join(" ");
process.stdout.write(`[command]${message}${os.EOL}`);
}
/**
* 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.
*/
async function exec(command, args) {
return new Promise((resolve, reject) => {
const proc = spawn(command, args, {
stdio: ["ignore", "inherit", "inherit"],
});
logCommand(proc.spawnfile, ...proc.spawnargs.splice(1));
proc.on("error", reject);
proc.on("close", (code) => {
if (code === 0) {
resolve();
}
else {
reject(new Error(`Command exited with status code ${code.toString()}`));
}
});
});
}
/**
* 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(...context.configure.options.map((opt) => "-D" + opt));
configureArgs.push(...context.configure.args);
await exec("cmake", configureArgs);
}
/**
* Builds a CMake project.
*
* 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.
*/
async function buildProject(context) {
await exec("cmake", ["--build", context.buildDir, ...context.build.args]);
}
const regex = /"([^"]*)"|'([^']*)'|`([^`]*)`|(\S+)/g;
/**
* Converts a space-separated string into a list of arguments.
*
* This function parses the provided string, which contains arguments separated by spaces and possibly enclosed in quotes, into a list of arguments.
*
* @param str - The space-separated string to parse.
* @returns A list of arguments.
*/
function parse(str) {
const args = [];
let match;
while ((match = regex.exec(str)) !== null) {
args.push(match[1] || match[2] || match[3] || match[4]);
}
return args;
}
function getContext() {
const sourceDir = getInput("source-dir");
const options = [];
let input = getInput("c-compiler");
if (input)
options.push(`CMAKE_C_COMPILER=${input}`);
input = getInput("cxx-compiler");
if (input)
options.push(`CMAKE_CXX_COMPILER=${input}`);
input = getInput("c-flags");
if (input) {
const flags = input.replaceAll(/\s+/g, " ");
options.push(`CMAKE_C_FLAGS=${flags}`);
}
input = getInput("cxx-flags");
if (input) {
const flags = input.replaceAll(/\s+/g, " ");
options.push(`CMAKE_CXX_FLAGS=${flags}`);
}
input = getInput("options");
if (input) {
options.push(...parse(input).map((opt) => opt));
}
return {
sourceDir,
buildDir: getInput("build-dir") || path.join(sourceDir, "build"),
configure: {
generator: getInput("generator"),
options,
args: parse(getInput("args")).map((arg) => arg),
},
build: {
enabled: getInput("run-build") == "true",
args: parse(getInput("build-args")).map((arg) => arg),
},
};
}
try {
const context = getContext();
await configureProject(context);
await setOutput("build-dir", context.buildDir);
if (context.build.enabled) {
await buildProject(context);
}
}
catch (err) {
logError(err);
process.exit(1);
}

28128
dist/index.js generated vendored Normal file

File diff suppressed because one or more lines are too long

3
dist/package.json generated vendored Normal file
View File

@ -0,0 +1,3 @@
{
"type": "module"
}

11
eslint.config.js Normal file
View File

@ -0,0 +1,11 @@
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
eslint.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.stylistic,
{
ignores: [".*", "dist"],
},
];

View File

@ -1,20 +0,0 @@
import eslint from "@eslint/js";
import { defineConfig, globalIgnores } from "eslint/config";
import tseslint from "typescript-eslint";
export default defineConfig(
globalIgnores(["dist"]),
eslint.configs.recommended,
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: {
allowDefaultProject: ["rollup.config.js"],
},
tsconfigRootDir: import.meta.dirname,
},
},
},
);

20
jest.config.json Normal file
View File

@ -0,0 +1,20 @@
{
"collectCoverage": true,
"coverageReporters": ["text"],
"coverageThreshold": {
"global": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
},
"moduleNameMapper": {
"^(\\.{1,2}/.*)\\.js$": "$1"
},
"preset": "ts-jest/presets/default-esm",
"transform": {
"^.+\\.ts$": ["ts-jest", { "useESM": true }]
},
"verbose": true
}

View File

@ -1,20 +0,0 @@
pre-commit:
piped: true
jobs:
- name: install dependencies
run: pnpm install
- name: check types
run: pnpm tsc
- name: fix formatting
run: pnpm prettier --write .
- name: fix lint
run: pnpm eslint --fix
- name: build action
run: pnpm rollup -c
- name: check diff
run: git diff --exit-code dist pnpm-lock.yaml {staged_files}

View File

@ -3,26 +3,27 @@
"private": true,
"type": "module",
"scripts": {
"test": "vitest run"
"build": "ncc build src/index.ts",
"format": "prettier --write --cache . !dist !README.md",
"lint": "eslint",
"test": "jest"
},
"dependencies": {
"gha-utils": "^0.4.1"
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1"
},
"devDependencies": {
"@eslint/js": "^9.35.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.4",
"@tsconfig/node24": "^24.0.1",
"@types/node": "^24.3.0",
"@vitest/coverage-v8": "^3.1.4",
"eslint": "^9.34.0",
"jiti": "^2.5.1",
"lefthook": "^1.12.3",
"prettier": "^3.6.2",
"prettier-plugin-organize-imports": "^4.2.0",
"rollup": "^4.46.2",
"typescript": "^5.9.2",
"typescript-eslint": "^8.42.0",
"vitest": "^3.1.4"
}
"@eslint/js": "^9.8.0",
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.12",
"@types/node": "^22.1.0",
"@vercel/ncc": "^0.38.1",
"eslint": "^9.8.0",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"ts-jest": "^29.2.4",
"typescript": "^5.5.4",
"typescript-eslint": "^8.0.0"
},
"packageManager": "yarn@4.4.0"
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +0,0 @@
packages:
- .
onlyBuiltDependencies:
- esbuild
- lefthook

View File

@ -1,11 +0,0 @@
import { nodeResolve } from "@rollup/plugin-node-resolve";
import typescript from "@rollup/plugin-typescript";
export default {
input: "src/action.ts",
output: {
dir: "dist",
entryFileNames: "[name].mjs",
},
plugins: [nodeResolve(), typescript()],
};

View File

@ -1,18 +0,0 @@
import { logError, setOutput } from "gha-utils";
import { buildProject, configureProject } from "./cmake.js";
import { getContext } from "./context.js";
try {
const context = getContext();
await configureProject(context);
await setOutput("build-dir", context.buildDir);
if (context.build.enabled) {
await buildProject(context);
}
} catch (err) {
logError(err);
process.exit(1);
}

View File

@ -1,29 +1,29 @@
import { describe, expect, it, vi } from "vitest";
import { buildProject, configureProject } from "./cmake.js";
import type { Context } from "./context.js";
import { exec } from "./exec.js";
import { jest } from "@jest/globals";
import type { Inputs } from "./inputs.js";
interface TestCase {
name: string;
context?: Partial<Context>;
inputs?: Partial<Inputs>;
expectedArgs: string[];
}
const defaultContext: Context = {
const defaultInputs: Inputs = {
sourceDir: "",
buildDir: "build",
configure: {
generator: "",
options: [],
args: [],
},
build: {
enabled: true,
args: [],
},
generator: "",
cCompiler: "",
cxxCompiler: "",
cFlags: "",
cxxFlags: "",
options: [],
args: [],
runBuild: true,
buildArgs: [],
};
vi.mock("./exec.js", () => ({ exec: vi.fn() }));
jest.unstable_mockModule("@actions/exec", () => ({
exec: jest.fn(),
}));
describe("configure a CMake project", () => {
const testCases: TestCase[] = [
@ -33,28 +33,42 @@ describe("configure a CMake project", () => {
},
{
name: "with source directory specified",
context: { sourceDir: "project" },
inputs: { sourceDir: "project" },
expectedArgs: ["project", "-B", "build"],
},
{
name: "with build directory specified",
context: { buildDir: "output" },
inputs: { buildDir: "output" },
expectedArgs: ["-B", "output"],
},
{
name: "with generator specified",
context: { configure: { generator: "Ninja", options: [], args: [] } },
inputs: { generator: "Ninja" },
expectedArgs: ["-B", "build", "-G", "Ninja"],
},
{
name: "with C compiler specified",
inputs: { cCompiler: "clang" },
expectedArgs: ["-B", "build", "-DCMAKE_C_COMPILER=clang"],
},
{
name: "with C++ compiler specified",
inputs: { cxxCompiler: "clang++" },
expectedArgs: ["-B", "build", "-DCMAKE_CXX_COMPILER=clang++"],
},
{
name: "with C flags specified",
inputs: { cFlags: "-Werror -Wall" },
expectedArgs: ["-B", "build", "-DCMAKE_C_FLAGS=-Werror -Wall"],
},
{
name: "with C++ flags specified",
inputs: { cxxFlags: "-Werror -Wall -Wextra" },
expectedArgs: ["-B", "build", "-DCMAKE_CXX_FLAGS=-Werror -Wall -Wextra"],
},
{
name: "with additional options specified",
context: {
configure: {
generator: "",
options: ["BUILD_TESTING=ON", "BUILD_EXAMPLES=ON"],
args: [],
},
},
inputs: { options: ["BUILD_TESTING=ON", "BUILD_EXAMPLES=ON"] },
expectedArgs: [
"-B",
"build",
@ -64,25 +78,21 @@ describe("configure a CMake project", () => {
},
{
name: "with additional arguments specified",
context: {
configure: {
generator: "",
options: [],
args: ["-Wdev", "-Wdeprecated"],
},
},
inputs: { args: ["-Wdev", "-Wdeprecated"] },
expectedArgs: ["-B", "build", "-Wdev", "-Wdeprecated"],
},
{
name: "with all specified",
context: {
inputs: {
sourceDir: "project",
buildDir: "output",
configure: {
generator: "Ninja",
options: ["BUILD_TESTING=ON", "BUILD_EXAMPLES=ON"],
args: ["-Wdev", "-Wdeprecated"],
},
generator: "Ninja",
cCompiler: "clang",
cxxCompiler: "clang++",
cFlags: "-Werror -Wall",
cxxFlags: "-Werror -Wall -Wextra",
options: ["BUILD_TESTING=ON", "BUILD_EXAMPLES=ON"],
args: ["-Wdev", "-Wdeprecated"],
},
expectedArgs: [
"project",
@ -90,6 +100,10 @@ describe("configure a CMake project", () => {
"output",
"-G",
"Ninja",
"-DCMAKE_C_COMPILER=clang",
"-DCMAKE_CXX_COMPILER=clang++",
"-DCMAKE_C_FLAGS=-Werror -Wall",
"-DCMAKE_CXX_FLAGS=-Werror -Wall -Wextra",
"-DBUILD_TESTING=ON",
"-DBUILD_EXAMPLES=ON",
"-Wdev",
@ -100,9 +114,13 @@ describe("configure a CMake project", () => {
for (const testCase of testCases) {
it(`should execute the correct command ${testCase.name}`, async () => {
vi.mocked(exec).mockReset();
const { configureProject } = await import("./cmake.js");
const { exec } = await import("@actions/exec");
await configureProject({ ...defaultContext, ...testCase.context });
jest.mocked(exec).mockReset();
const prom = configureProject({ ...defaultInputs, ...testCase.inputs });
await expect(prom).resolves.toBeUndefined();
expect(exec).toHaveBeenCalledTimes(1);
expect(exec).toHaveBeenLastCalledWith("cmake", testCase.expectedArgs);
@ -118,22 +136,19 @@ describe("build a CMake project", () => {
},
{
name: "with build directory specified",
context: { buildDir: "output" },
inputs: { buildDir: "output" },
expectedArgs: ["--build", "output"],
},
{
name: "with additional arguments specified",
context: { build: { enabled: true, args: ["--target", "foo"] } },
inputs: { buildArgs: ["--target", "foo"] },
expectedArgs: ["--build", "build", "--target", "foo"],
},
{
name: "with all specified",
context: {
inputs: {
buildDir: "output",
build: {
enabled: true,
args: ["--target", "foo"],
},
buildArgs: ["--target", "foo"],
},
expectedArgs: ["--build", "output", "--target", "foo"],
},
@ -141,9 +156,13 @@ describe("build a CMake project", () => {
for (const testCase of testCases) {
it(`should execute the correct command ${testCase.name}`, async () => {
vi.mocked(exec).mockReset();
const { buildProject } = await import("./cmake.js");
const { exec } = await import("@actions/exec");
await buildProject({ ...defaultContext, ...testCase.context });
jest.mocked(exec).mockReset();
const prom = buildProject({ ...defaultInputs, ...testCase.inputs });
await expect(prom).resolves.toBeUndefined();
expect(exec).toHaveBeenCalledTimes(1);
expect(exec).toHaveBeenLastCalledWith("cmake", testCase.expectedArgs);

View File

@ -1,43 +1,51 @@
import type { Context } from "./context.js";
import { exec } from "./exec.js";
import { exec } from "@actions/exec";
import type { Inputs } from "./inputs.js";
/**
* Configures the build system for a CMake project.
* Configures the build system of 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.
* @param inputs - The action inputs.
*/
export async function configureProject(context: Context): Promise<void> {
export async function configureProject(inputs: Inputs): Promise<void> {
const configureArgs = [];
if (context.sourceDir) {
configureArgs.push(context.sourceDir);
if (inputs.sourceDir) {
configureArgs.push(inputs.sourceDir);
}
configureArgs.push("-B", context.buildDir);
configureArgs.push("-B", inputs.buildDir);
if (context.configure.generator) {
configureArgs.push("-G", context.configure.generator);
if (inputs.generator) {
configureArgs.push(...["-G", inputs.generator]);
}
configureArgs.push(...context.configure.options.map((opt) => "-D" + opt));
configureArgs.push(...context.configure.args);
if (inputs.cCompiler) {
configureArgs.push("-DCMAKE_C_COMPILER=" + inputs.cCompiler);
}
if (inputs.cxxCompiler) {
configureArgs.push("-DCMAKE_CXX_COMPILER=" + inputs.cxxCompiler);
}
if (inputs.cFlags) {
configureArgs.push("-DCMAKE_C_FLAGS=" + inputs.cFlags);
}
if (inputs.cxxFlags) {
configureArgs.push("-DCMAKE_CXX_FLAGS=" + inputs.cxxFlags);
}
configureArgs.push(...inputs.options.map((opt) => "-D" + opt));
configureArgs.push(...inputs.args);
await exec("cmake", configureArgs);
}
/**
* Builds a CMake project.
* Build a CMake project.
*
* 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.
* @param inputs - The action inputs.
*/
export async function buildProject(context: Context): Promise<void> {
await exec("cmake", ["--build", context.buildDir, ...context.build.args]);
export async function buildProject(inputs: Inputs): Promise<void> {
await exec("cmake", ["--build", inputs.buildDir, ...inputs.buildArgs]);
}

View File

@ -1,205 +0,0 @@
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import type { Context } from "./context.js";
vi.mock("gha-utils", () => ({ getInput: vi.fn() }));
describe("get action context", () => {
interface TestCase {
name: string;
inputs?: Record<string, string>;
expectedContext?: Partial<Context>;
}
const testCases: TestCase[] = [
{
name: "with nothing specified",
},
{
name: "with source directory specified",
inputs: { "source-dir": "project" },
expectedContext: {
sourceDir: "project",
buildDir: path.join("project", "build"),
},
},
{
name: "with build directory specified",
inputs: { "build-dir": "output" },
expectedContext: { buildDir: "output" },
},
{
name: "with source and build directories specified",
inputs: {
"source-dir": "project",
"build-dir": "output",
},
expectedContext: {
sourceDir: "project",
buildDir: "output",
},
},
{
name: "with generator specified",
inputs: { generator: "Ninja" },
expectedContext: {
configure: {
generator: "Ninja",
options: [],
args: [],
},
},
},
{
name: "with C compiler specified",
inputs: { "c-compiler": "clang" },
expectedContext: {
configure: {
generator: "",
options: ["CMAKE_C_COMPILER=clang"],
args: [],
},
},
},
{
name: "with C++ compiler specified",
inputs: { "cxx-compiler": "clang++" },
expectedContext: {
configure: {
generator: "",
options: ["CMAKE_CXX_COMPILER=clang++"],
args: [],
},
},
},
{
name: "with C flags specified",
inputs: { "c-flags": "-Werror -Wall\n-Wextra" },
expectedContext: {
configure: {
generator: "",
options: ["CMAKE_C_FLAGS=-Werror -Wall -Wextra"],
args: [],
},
},
},
{
name: "with C++ flags specified",
inputs: { "cxx-flags": "-Werror -Wall\n-Wextra -Wpedantic" },
expectedContext: {
configure: {
generator: "",
options: ["CMAKE_CXX_FLAGS=-Werror -Wall -Wextra -Wpedantic"],
args: [],
},
},
},
{
name: "with additional options specified",
inputs: {
options: `BUILD_TESTING=ON BUILD_EXAMPLES=ON\nBUILD_DOCS=ON "FOO=BAR BAZ"`,
},
expectedContext: {
configure: {
generator: "",
options: [
"BUILD_TESTING=ON",
"BUILD_EXAMPLES=ON",
"BUILD_DOCS=ON",
"FOO=BAR BAZ",
],
args: [],
},
},
},
{
name: "with additional arguments specified",
inputs: { args: `-Wdev -Wdeprecated\n--fresh --foo "bar baz"` },
expectedContext: {
configure: {
generator: "",
options: [],
args: ["-Wdev", "-Wdeprecated", "--fresh", "--foo", "bar baz"],
},
},
},
{
name: "with run build specified",
inputs: { "run-build": "true" },
expectedContext: { build: { enabled: true, args: [] } },
},
{
name: "with additional build arguments specified",
inputs: { "build-args": `--target foo\n--parallel 8 --foo "bar baz"` },
expectedContext: {
build: {
enabled: false,
args: ["--target", "foo", "--parallel", "8", "--foo", "bar baz"],
},
},
},
{
name: "with all specified",
inputs: {
"source-dir": "project",
"build-dir": "output",
generator: "Ninja",
"c-compiler": "clang",
"cxx-compiler": "clang++",
"c-flags": "-Werror -Wall\n-Wextra",
"cxx-flags": "-Werror -Wall\n-Wextra -Wpedantic",
options: `BUILD_TESTING=ON BUILD_EXAMPLES=ON\nBUILD_DOCS=ON "FOO=BAR BAZ"`,
args: `-Wdev -Wdeprecated\n--fresh --foo "bar baz"`,
"run-build": "true",
"build-args": `--target foo\n--parallel 8 --foo "bar baz"`,
},
expectedContext: {
sourceDir: "project",
buildDir: "output",
configure: {
generator: "Ninja",
options: [
"CMAKE_C_COMPILER=clang",
"CMAKE_CXX_COMPILER=clang++",
"CMAKE_C_FLAGS=-Werror -Wall -Wextra",
"CMAKE_CXX_FLAGS=-Werror -Wall -Wextra -Wpedantic",
"BUILD_TESTING=ON",
"BUILD_EXAMPLES=ON",
"BUILD_DOCS=ON",
"FOO=BAR BAZ",
],
args: ["-Wdev", "-Wdeprecated", "--fresh", "--foo", "bar baz"],
},
build: {
enabled: true,
args: ["--target", "foo", "--parallel", "8", "--foo", "bar baz"],
},
},
},
];
for (const testCase of testCases) {
it(`should get the action context ${testCase.name}`, async () => {
const { getInput } = await import("gha-utils");
const { getContext } = await import("./context.js");
const inputs = testCase.inputs ?? {};
vi.mocked(getInput).mockImplementation((name) => inputs[name] ?? "");
expect(getContext()).toStrictEqual({
sourceDir: "",
buildDir: "build",
configure: {
generator: "",
options: [],
args: [],
},
build: {
enabled: false,
args: [],
},
...testCase.expectedContext,
});
});
}
});

View File

@ -1,59 +0,0 @@
import { getInput } from "gha-utils";
import path from "node:path";
import { parse } from "./utils.js";
export interface Context {
sourceDir: string;
buildDir: string;
configure: {
generator: string;
options: string[];
args: string[];
};
build: {
enabled: boolean;
args: string[];
};
}
export function getContext(): Context {
const sourceDir = getInput("source-dir");
const options: string[] = [];
let input = getInput("c-compiler");
if (input) options.push(`CMAKE_C_COMPILER=${input}`);
input = getInput("cxx-compiler");
if (input) options.push(`CMAKE_CXX_COMPILER=${input}`);
input = getInput("c-flags");
if (input) {
const flags = input.replaceAll(/\s+/g, " ");
options.push(`CMAKE_C_FLAGS=${flags}`);
}
input = getInput("cxx-flags");
if (input) {
const flags = input.replaceAll(/\s+/g, " ");
options.push(`CMAKE_CXX_FLAGS=${flags}`);
}
input = getInput("options");
if (input) {
options.push(...parse(input).map((opt) => opt));
}
return {
sourceDir,
buildDir: getInput("build-dir") || path.join(sourceDir, "build"),
configure: {
generator: getInput("generator"),
options,
args: parse(getInput("args")).map((arg) => arg),
},
build: {
enabled: getInput("run-build") == "true",
args: parse(getInput("build-args")).map((arg) => arg),
},
};
}

View File

@ -1,29 +0,0 @@
import { logCommand } from "gha-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { exec } from "./exec.js";
describe("execute commands", () => {
vi.mock("gha-utils", () => ({
logCommand: vi.fn<(command: string, ...args: string[]) => void>(),
}));
beforeEach(() => {
vi.mocked(logCommand).mockClear();
});
it("should successfully execute a command", async () => {
await exec("node", ["--version"]);
expect(logCommand).toHaveBeenCalledTimes(1);
expect(logCommand).toHaveBeenCalledWith("node", "--version");
});
it("should fail to execute a command", async () => {
await expect(exec("node", ["--invalid"])).rejects.toThrow(
"Command exited with status code 9",
);
expect(logCommand).toHaveBeenCalledTimes(1);
expect(logCommand).toHaveBeenCalledWith("node", "--invalid");
});
});

View File

@ -1,28 +0,0 @@
import { logCommand } from "gha-utils";
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<void> {
return new Promise<void>((resolve, reject) => {
const proc = spawn(command, args, {
stdio: ["ignore", "inherit", "inherit"],
});
logCommand(proc.spawnfile, ...proc.spawnargs.splice(1));
proc.on("error", reject);
proc.on("close", (code: number) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Command exited with status code ${code.toString()}`));
}
});
});
}

17
src/index.ts Normal file
View File

@ -0,0 +1,17 @@
import * as core from "@actions/core";
import { buildProject, configureProject } from "./cmake.js";
import { getInputs } from "./inputs.js";
try {
const inputs = getInputs();
await configureProject(inputs);
core.setOutput("build-dir", inputs.buildDir);
if (inputs.runBuild) {
await buildProject(inputs);
}
} catch (err) {
core.setFailed(err);
}

168
src/inputs.test.ts Normal file
View File

@ -0,0 +1,168 @@
import { jest } from "@jest/globals";
import path from "node:path";
import type { Inputs } from "./inputs.js";
jest.unstable_mockModule("@actions/core", () => ({
getBooleanInput: jest.fn(),
getInput: jest.fn(),
getMultilineInput: jest.fn(),
}));
describe("get action inputs", () => {
interface TestCase {
name: string;
booleanInputs?: Record<string, boolean>;
stringInputs?: Record<string, string>;
multilineInputs?: Record<string, string[]>;
expectedInputs?: Partial<Inputs>;
}
const testCases: TestCase[] = [
{
name: "with nothing specified",
},
{
name: "with source directory specified",
stringInputs: { "source-dir": "project" },
expectedInputs: {
sourceDir: "project",
buildDir: path.join("project", "build"),
},
},
{
name: "with build directory specified",
stringInputs: { "build-dir": "output" },
expectedInputs: { buildDir: "output" },
},
{
name: "with source and build directories specified",
stringInputs: {
"source-dir": "project",
"build-dir": "output",
},
expectedInputs: {
sourceDir: "project",
buildDir: "output",
},
},
{
name: "with generator specified",
stringInputs: { generator: "Ninja" },
expectedInputs: { generator: "Ninja" },
},
{
name: "with C compiler specified",
stringInputs: { "c-compiler": "clang" },
expectedInputs: { cCompiler: "clang" },
},
{
name: "with C++ compiler specified",
stringInputs: { "cxx-compiler": "clang++" },
expectedInputs: { cxxCompiler: "clang++" },
},
{
name: "with C flags specified",
multilineInputs: { "c-flags": ["-Werror -Wall", "-Wextra"] },
expectedInputs: { cFlags: "-Werror -Wall -Wextra" },
},
{
name: "with C++ flags specified",
multilineInputs: { "cxx-flags": ["-Werror -Wall", "-Wextra -Wpedantic"] },
expectedInputs: { cxxFlags: "-Werror -Wall -Wextra -Wpedantic" },
},
{
name: "with additional options specified",
multilineInputs: {
options: ["BUILD_TESTING=ON BUILD_EXAMPLES=ON", "BUILD_DOCS=ON"],
},
expectedInputs: {
options: ["BUILD_TESTING=ON", "BUILD_EXAMPLES=ON", "BUILD_DOCS=ON"],
},
},
{
name: "with additional arguments specified",
multilineInputs: { args: ["-Wdev -Wdeprecated", "--fresh"] },
expectedInputs: { args: ["-Wdev", "-Wdeprecated", "--fresh"] },
},
{
name: "with run build specified",
booleanInputs: { "run-build": false },
expectedInputs: { runBuild: false },
},
{
name: "with additional build arguments specified",
multilineInputs: { "build-args": ["--target foo", "--parallel 8"] },
expectedInputs: { buildArgs: ["--target", "foo", "--parallel", "8"] },
},
{
name: "with all specified",
booleanInputs: {
"run-build": false,
},
stringInputs: {
"source-dir": "project",
"build-dir": "output",
generator: "Ninja",
"c-compiler": "clang",
"cxx-compiler": "clang++",
},
multilineInputs: {
"c-flags": ["-Werror -Wall", "-Wextra"],
"cxx-flags": ["-Werror -Wall", "-Wextra -Wpedantic"],
options: ["BUILD_TESTING=ON BUILD_EXAMPLES=ON", "BUILD_DOCS=ON"],
args: ["-Wdev -Wdeprecated", "--fresh"],
"build-args": ["--target foo", "--parallel 8"],
},
expectedInputs: {
sourceDir: "project",
buildDir: "output",
generator: "Ninja",
cCompiler: "clang",
cxxCompiler: "clang++",
cFlags: "-Werror -Wall -Wextra",
cxxFlags: "-Werror -Wall -Wextra -Wpedantic",
options: ["BUILD_TESTING=ON", "BUILD_EXAMPLES=ON", "BUILD_DOCS=ON"],
args: ["-Wdev", "-Wdeprecated", "--fresh"],
runBuild: false,
buildArgs: ["--target", "foo", "--parallel", "8"],
},
},
];
for (const testCase of testCases) {
it(`should get the action inputs ${testCase.name}`, async () => {
const { getInputs } = await import("./inputs.js");
const core = await import("@actions/core");
const booleanInputs = { "run-build": true, ...testCase.booleanInputs };
jest.mocked(core.getBooleanInput).mockImplementation((name) => {
return booleanInputs[name] ?? false;
});
const stringInputs = { ...testCase.stringInputs };
jest.mocked(core.getInput).mockImplementation((name) => {
return stringInputs[name] ?? "";
});
const multilineInputs = { ...testCase.multilineInputs };
jest.mocked(core.getMultilineInput).mockImplementation((name) => {
return multilineInputs[name] ?? [];
});
expect(getInputs()).toStrictEqual({
sourceDir: "",
buildDir: "build",
generator: "",
cCompiler: "",
cxxCompiler: "",
cFlags: "",
cxxFlags: "",
options: [],
args: [],
runBuild: true,
buildArgs: [],
...testCase.expectedInputs,
});
});
}
});

35
src/inputs.ts Normal file
View File

@ -0,0 +1,35 @@
import { getBooleanInput, getInput, getMultilineInput } from "@actions/core";
import path from "node:path";
export interface Inputs {
sourceDir: string;
buildDir: string;
generator: string;
cCompiler: string;
cxxCompiler: string;
cFlags: string;
cxxFlags: string;
options: string[];
args: string[];
runBuild: boolean;
buildArgs: string[];
}
export function getInputs(): Inputs {
const sourceDir = getInput("source-dir");
return {
sourceDir,
buildDir: getInput("build-dir") || path.join(sourceDir, "build"),
generator: getInput("generator"),
cCompiler: getInput("c-compiler"),
cxxCompiler: getInput("cxx-compiler"),
cFlags: getMultilineInput("c-flags").join(" "),
cxxFlags: getMultilineInput("cxx-flags").join(" "),
options: getMultilineInput("options").flatMap((opts) => opts.split(" ")),
args: getMultilineInput("args").flatMap((args) => args.split(" ")),
runBuild: getBooleanInput("run-build"),
buildArgs: getMultilineInput("build-args").flatMap((args) =>
args.split(" "),
),
};
}

View File

@ -1,18 +0,0 @@
const regex = /"([^"]*)"|'([^']*)'|`([^`]*)`|(\S+)/g;
/**
* Converts a space-separated string into a list of arguments.
*
* This function parses the provided string, which contains arguments separated by spaces and possibly enclosed in quotes, into a list of arguments.
*
* @param str - The space-separated string to parse.
* @returns A list of arguments.
*/
export function parse(str: string): string[] {
const args: string[] = [];
let match: RegExpExecArray | null;
while ((match = regex.exec(str)) !== null) {
args.push(match[1] || match[2] || match[3] || match[4]);
}
return args;
}

View File

@ -1,7 +1,8 @@
{
"extends": "@tsconfig/node24",
"compilerOptions": {
"module": "node16",
"noEmit": true
"moduleResolution": "node16",
"esModuleInterop": true,
"target": "es2022"
}
}

View File

@ -1,12 +0,0 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
all: false,
enabled: true,
reporter: ["text"],
thresholds: { 100: true },
},
},
});

4548
yarn.lock generated Normal file

File diff suppressed because it is too large Load Diff