Compare commits

..

No commits in common. "main" and "v1.1.0" have entirely different histories.
main ... v1.1.0

31 changed files with 330 additions and 3548 deletions

2
.gitattributes vendored
View File

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

View File

@ -1,18 +0,0 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
commit-message:
prefix: chore
labels: []
- package-ecosystem: npm
directory: /
schedule:
interval: daily
commit-message:
prefix: chore
labels: []
versioning-strategy: increase

6
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily

View File

@ -1,25 +0,0 @@
name: Build
on:
workflow_dispatch:
pull_request:
push:
branches: [main]
jobs:
build-package:
name: Build Package
runs-on: ubuntu-24.04
steps:
- name: Checkout Project
uses: actions/checkout@v4.2.2
- name: Setup pnpm
uses: threeal/setup-pnpm-action@v1.0.0
- name: Install Dependencies
run: pnpm install
- name: Build Package
run: pnpm build
- name: Check Diff
run: git diff && git diff-index --quiet --exit-code HEAD

View File

@ -1,28 +0,0 @@
name: Check
on:
workflow_dispatch:
pull_request:
push:
branches: [main]
jobs:
check-package:
name: Check Package
runs-on: ubuntu-24.04
steps:
- name: Checkout Project
uses: actions/checkout@v4.2.2
- name: Setup pnpm
uses: threeal/setup-pnpm-action@v1.0.0
- name: Install Dependencies
run: pnpm install
- name: Check Formatting
run: pnpm format
- name: Check Diff
run: git diff && git diff-index --quiet --exit-code HEAD
- name: Check Lint
run: pnpm lint

View File

@ -1,168 +0,0 @@
name: Test
on:
workflow_dispatch:
pull_request:
push:
branches: [main]
jobs:
test-package:
name: Test Package
runs-on: ubuntu-24.04
steps:
- name: Checkout Project
uses: actions/checkout@v4.2.2
- name: Setup pnpm
uses: threeal/setup-pnpm-action@v1.0.0
- name: Install Dependencies
run: pnpm install
- name: Test Package
run: pnpm test
test-action:
name: Test Action
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-14, windows-2022]
steps:
- name: Checkout Sample Project
uses: actions/checkout@v4.2.2
with:
repository: threeal/cpp-starter
ref: v1.0.0
- name: Checkout Action
uses: actions/checkout@v4.2.2
with:
path: cmake-action
sparse-checkout: |
action.yml
dist
sparse-checkout-cone-mode: false
- name: Build Sample Project
id: cmake-action
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
test-action-with-specified-dirs:
name: Test Action With Specified Directories
runs-on: ubuntu-24.04
steps:
- name: Checkout Sample Project
uses: actions/checkout@v4.2.2
with:
repository: threeal/cpp-starter
ref: v1.0.0
path: project
- name: Checkout Action
uses: actions/checkout@v4.2.2
with:
path: cmake-action
sparse-checkout: |
action.yml
dist
sparse-checkout-cone-mode: false
- name: Build Sample Project
id: cmake-action
uses: ./cmake-action
with:
source-dir: project
build-dir: output
- name: Run Sample Project
run: output/generate_sequence 5
test-action-without-run-build:
name: Test Action Without Run Build
runs-on: ubuntu-24.04
steps:
- name: Checkout Sample Project
uses: actions/checkout@v4.2.2
with:
repository: threeal/cpp-starter
ref: v1.0.0
- name: Checkout Action
uses: actions/checkout@v4.2.2
with:
path: cmake-action
sparse-checkout: |
action.yml
dist
sparse-checkout-cone-mode: false
- name: Modify Sample Project
run: echo 'invalid' >> src/main.cpp
- name: Configure Sample Project
uses: ./cmake-action
with:
run-build: false
test-action-with-additional-options:
name: Test Action With Additional Options
runs-on: ubuntu-24.04
steps:
- name: Checkout Sample Project
uses: actions/checkout@v4.2.2
with:
repository: threeal/cpp-starter
ref: v1.0.0
- name: Checkout Action
uses: actions/checkout@v4.2.2
with:
path: cmake-action
sparse-checkout: |
action.yml
dist
sparse-checkout-cone-mode: false
- name: Build Sample Project
uses: ./cmake-action
with:
options: BUILD_TESTING=ON
- name: Test Sample Project
uses: threeal/ctest-action@v1.1.0
test-action-with-custom-generator:
name: Test Action With Custom Generator
runs-on: ubuntu-24.04
steps:
- name: Checkout Sample Project
uses: actions/checkout@v4.2.2
with:
repository: threeal/cpp-starter
ref: v1.0.0
- name: Checkout Action
uses: actions/checkout@v4.2.2
with:
path: cmake-action
sparse-checkout: |
action.yml
dist
sparse-checkout-cone-mode: false
- name: Setup Ninja
uses: seanmiddleditch/gha-setup-ninja@v6
- name: Configure Sample Project
id: cmake-action
uses: ./cmake-action
with:
generator: Ninja
run-build: false
- name: Build Sample Project
run: ninja -C ${{ steps.cmake-action.outputs.build-dir }}

97
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,97 @@
name: test
on:
workflow_dispatch:
push:
jobs:
default-usage:
runs-on: ${{ matrix.os }}-latest
strategy:
matrix:
os: [windows, ubuntu, macos]
steps:
- name: Check out this repository
uses: actions/checkout@v3.3.0
- name: Move test project to the working directory
run: mv test/* .
- name: Use this action
uses: ./
- name: Run the build result
run: ${{ matrix.os == 'windows' && 'build\Debug\hello_world.exe' || 'build/hello_world' }}
specified-dir-usage:
runs-on: ubuntu-latest
steps:
- name: Check out this repository
uses: actions/checkout@v3.3.0
- name: Use this action with specified directories
uses: ./
with:
source-dir: test
build-dir: output
run-test: true
test-args: -R hello_world
- name: Check if the default build directory does not exist
run: test ! -d build && test ! -d test/build
additional-flags-usage:
runs-on: ${{ matrix.compiler == 'msvc' && 'windows' || 'ubuntu' }}-latest
strategy:
matrix:
compiler: [gcc, msvc]
steps:
- name: Check out this repository
uses: actions/checkout@v3.3.0
- name: Use this action with additional compiler flags
uses: ./
with:
source-dir: test
targets: test_c test_cpp
run-test: true
c-flags: ${{ matrix.compiler == 'msvc' && '/w /WX-' || '-Wno-unused-variable' }}
cxx-flags: ${{ matrix.compiler == 'msvc' && '/w /WX-' || '-Wno-unused-variable' }}
args: -D CHECK_SURPASS_WARNING=ON
test-args: -R test ${{ matrix.compiler == 'msvc' && '-C Debug' || '' }}
specified-compiler-usage:
runs-on: ${{ matrix.os }}-latest
strategy:
matrix:
os: [windows, ubuntu, macos]
steps:
- name: Check out this repository
uses: actions/checkout@v3.3.0
- name: Use this action with specified compilers
uses: ./
with:
source-dir: test
targets: test_c test_cpp
run-test: true
generator: Ninja
c-compiler: clang
cxx-compiler: clang++
args: -D CHECK_USING_CLANG=ON
test-args: -R test
specified-generator-usage:
runs-on: ${{ matrix.os }}-latest
strategy:
matrix:
os: [windows, ubuntu, macos]
steps:
- name: Check out this repository
uses: actions/checkout@v3.3.0
- name: Use this action with a specified generator
uses: ./
with:
source-dir: test
run-test: true
generator: Ninja
test-args: -R hello_world

7
.gitignore vendored
View File

@ -1,6 +1 @@
.*
!.git*
!.npmrc
!.prettierignore
node_modules/
build

1
.npmrc
View File

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

View File

@ -1 +0,0 @@
pnpm-lock.yaml

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023-2025 Alfi Maulana
Copyright (c) 2023 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

137
README.md
View File

@ -1,106 +1,107 @@
# CMake Action
Configure and build [CMake](https://cmake.org/) projects on [GitHub Actions](https://github.com/features/actions).
[![latest version](https://img.shields.io/github/v/release/threeal/cmake-action)](https://github.com/threeal/cmake-action/releases/)
[![license](https://img.shields.io/github/license/threeal/cmake-action)](./LICENSE)
[![test status](https://img.shields.io/github/actions/workflow/status/threeal/cmake-action/test.yml?label=test&branch=main)](https://github.com/threeal/cmake-action/actions/workflows/test.yml)
This action wraps the [`cmake`](https://cmake.org/cmake/help/latest/manual/cmake.1.html) command for configuring and building CMake projects. It provides a more streamlined syntax for specifying build options compared to calling the `cmake` command directly.
Configure, build, and test a [CMake](https://cmake.org/) project on [GitHub Actions](https://github.com/features/actions).
Use this action to simplify the workflow run of your CMake project.
This action will configure a build environment for your project using the `cmake` command,
then it will build your project by running a `cmake --build` command,
and last it could test your project using the `ctest` command.
## Available Inputs
## Features
- Configure and build a project using the [cmake](https://cmake.org/cmake/help/latest/manual/cmake.1.html) command.
- Optionally test a project using the [ctest](https://cmake.org/cmake/help/latest/manual/ctest.1.html) command.
- Auto-detect and install required dependencies.
- Specify multiple CMake options directly from the Action inputs.
## Usage
For more information, see [action.yml](./action.yml) and [GitHub Actions guide](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions).
### Inputs
| Name | Value Type | Description |
| --- | --- | --- |
| `source-dir` | Path | The source directory of the CMake project. Defaults to the current working directory. |
| `build-dir` | Path | The build directory of the CMake project. Defaults to the `build` directory inside the source directory. |
| `generator` | String | The build system generator for the CMake project. Equivalent to setting the `-G` option. |
| `c-compiler` | String | The preferred executable for compiling C language files. Equivalent to defining the `CMAKE_C_COMPILER` variable. |
| `cxx-compiler` | String | The preferred executable for compiling C++ language files. Equivalent to defining the `CMAKE_CXX_COMPILER` variable. |
| `c-flags` | Multiple strings | Additional flags to pass when compiling C language files. Equivalent to defining the `CMAKE_C_FLAGS` variable. |
| `cxx-flags` | Multiple strings | Additional flags to pass when compiling C++ language files. Equivalent to defining the `CMAKE_CXX_FLAGS` variable. |
| `options` | Multiple strings | Additional options to pass during the CMake configuration. Equivalent to setting the `-D` option. |
| `args` | Multiple strings | Additional arguments to pass during the CMake configuration. |
| `run-build` | `true` or `false` | If enabled, builds the project using CMake. Defaults to `true`. |
| `build-args` | Multiple strings | Additional arguments to pass during the CMake build. |
| `source-dir` | Path | Source directory of the CMake project. Defaults to current directory. |
| `build-dir` | Path | Build directory of the CMake project. Defaults to `build` directory inside the source directory. |
| `targets` | Multiple strings | List of build targets. |
| `run-test` | `true` or `false` | If enabled, run testing using [CTest](https://cmake.org/cmake/help/latest/manual/ctest.1.html). Defaults to `false`. |
| `generator` | String | Build system generator of the CMake project. |
| `c-compiler` | String | Preferred executable for compiling C language files. |
| `cxx-compiler` | String | Preferred executable for compiling C++ language files. |
| `c-flags` | Multiple strings | Additional flags passed when compiling C language files. |
| `cxx-flags` | Multiple strings | Additional flags passed when compiling C++ language files. |
| `args` | Multiple strings | Additional arguments passed during the CMake configuration. |
| `test-args` | Multiple strings | Additional arguments passed during the CTest run. |
## Available Outputs
> Note: Multiple strings mean that the input could be specified with more than one value. Separate each value with a space or a new line.
| Name | Value Type | Description |
| --- | --- | --- |
| `build-dir` | Path | The build directory of the CMake project. |
> Note: All inputs are optional.
## Example Usages
This example demonstrates how to use this action to configure and build a CMake project in a GitHub Actions workflow:
### Examples
```yaml
name: Build
name: build
on:
push:
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
- name: Check out this repository
uses: actions/checkout@v3.3.0
- name: Build Project
uses: threeal/cmake-action@v2.1.0
- name: Configure and build this project
uses: threeal/cmake-action@latest
```
### Specify the Source and Build Directories
> Note: You can replace `@latest` with any version you like. See [this](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsuses).
By default, this action uses the current working directory as the source directory and the `build` directory inside the source directory as the build directory. To use different directories, set the `source-dir` and/or `build-dir` inputs:
#### Specify the Source and the Build Directories
```yaml
- name: Build Project
uses: threeal/cmake-action@v2.1.0
- name: Configure and build this project
uses: threeal/cmake-action@latest
with:
source-dir: source
build-dir: output
source-dir: submodules
build-dir: submodules/out
```
### Specify Build System Generator and Compiler
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:
#### Specify the Build Targets
```yaml
- name: Setup Ninja
uses: seanmiddleditch/gha-setup-ninja@v5
- name: Configure and build this project
uses: threeal/cmake-action@latest
with:
targets: hello_mars hello_sun
```
- name: Build Project
uses: threeal/cmake-action@v2.1.0
#### Run Unit Tests After Build
```yaml
- name: Configure, build, and test this project
uses: threeal/cmake-action@latest
with:
args: -DBUILD_TESTING=ON
run-test: true
```
#### Using Ninja as the Generator and Clang as the Compiler
```yaml
- name: Configure and build this project
uses: threeal/cmake-action@latest
with:
generator: Ninja
c-compiler: clang
cxx-compiler: clang++
```
### Specify Additional Options
Use the `options` input to specify additional options for configuring a project:
```yaml
- name: Build Project
uses: threeal/cmake-action@v2.1.0
with:
options: |
BUILD_TESTS=ON
BUILD_EXAMPLES=ON
```
The above example is equivalent to calling the `cmake` command with the `-DBUILD_TESTS=ON` and `-DBUILD_EXAMPLES=ON` arguments.
### Configure Project Without Building
By default, this action builds the project after configuration. To skip the build process, set the `run-build` option to `false`:
```yaml
- name: Configure Project
uses: threeal/cmake-action@v2.1.0
with:
run-build: false
```
## License
This project is licensed under the terms of the [MIT License](./LICENSE).
Copyright © 2023-2025 [Alfi Maulana](https://github.com/threeal/)
Copyright © 2023 [Alfi Maulana](https://github.com/threeal/)

View File

@ -1,37 +1,114 @@
name: CMake Action
description: Configure and build CMake projects
description: Configure, build, and test a CMake project
author: Alfi Maulana
branding:
color: gray-dark
icon: terminal
inputs:
source-dir:
description: The source directory of the CMake project
description: Source directory of the CMake project
required: false
build-dir:
description: The build directory of the CMake project
description: Build directory of the CMake project
required: false
targets:
description: List of build targets
required: false
run-test:
description: If enabled, run testing using CTest (true/false)
required: false
default: false
generator:
description: The build system generator for the CMake project
description: Build system generator of the CMake project
required: false
c-compiler:
description: The preferred executable for compiling C language files
description: Preferred executable for compiling C language files
required: false
cxx-compiler:
description: The preferred executable for compiling C++ language files
description: Preferred executable for compiling C++ language files
required: false
c-flags:
description: Additional flags to pass when compiling C language files
description: Additional flags passed when compiling C language files
required: false
cxx-flags:
description: Additional flags to pass when compiling C++ language files
options:
description: Additional options to pass during the CMake configuration
description: Additional flags passed when compiling C++ language files
required: false
args:
description: Additional arguments to pass during the CMake configuration
run-build:
description: If enabled, builds the project using CMake
default: true
build-args:
description: Additional arguments to pass during the CMake build
outputs:
build-dir:
description: The build directory of the CMake project
value: ${{ steps.process-inputs.outputs.build-dir }}
description: Additional arguments passed during the CMake configuration
required: false
test-args:
description: Additional arguments passed during the CTest run
required: false
runs:
using: node20
main: dist/action.mjs
using: composite
steps:
- name: Process the inputs
id: process_inputs
shell: bash
run: |
SOURCE_DIR="."
if [ -n '${{ inputs.source-dir }}' ]; then
SOURCE_DIR="${{ inputs.source-dir }}"
fi
BUILD_DIR="build"
if [ -n '${{ inputs.build-dir }}' ]; then
BUILD_DIR="${{ inputs.build-dir }}"
elif [ -n "${{ inputs.source-dir }}" ]; then
BUILD_DIR="${{ inputs.source-dir }}/build"
fi
ARGS="'$SOURCE_DIR' -B '$BUILD_DIR'"
BUILD_ARGS="--build '$BUILD_DIR'"
TEST_ARGS=""
if [ -n '${{ inputs.targets }}' ]; then
BUILD_ARGS="$BUILD_ARGS --target ${{ inputs.targets }}"
fi
if [ '${{ inputs.run-test }}' == 'true' ]; then
TEST_ARGS="--test-dir '$BUILD_DIR' --output-on-failure --no-tests=error"
fi
if [ -n '${{ inputs.generator }}' ]; then
ARGS="$ARGS -G '${{ inputs.generator }}'"
fi
if [ -n '${{ inputs.c-compiler }}' ]; then
ARGS="$ARGS -D CMAKE_C_COMPILER='${{ inputs.c-compiler }}'"
fi
if [ -n '${{ inputs.cxx-compiler }}' ]; then
ARGS="$ARGS -D CMAKE_CXX_COMPILER='${{ inputs.cxx-compiler }}'"
fi
if [ -n '${{ inputs.c-flags }}' ]; then
ARGS="$ARGS -D CMAKE_C_FLAGS='${{ inputs.c-flags }}'"
fi
if [ -n '${{ inputs.cxx-flags }}' ]; then
ARGS="$ARGS -D CMAKE_CXX_FLAGS='${{ inputs.cxx-flags }}'"
fi
if [ -n '${{ inputs.args }}' ]; then
ARGS="$ARGS ${{ inputs.args }}"
fi
if [ -n '${{ inputs.test-args }}' ]; then
TEST_ARGS="$TEST_ARGS ${{ inputs.test-args }}"
fi
echo "cmake_args=${ARGS//[$'\t\r\n']}" >> $GITHUB_OUTPUT
echo "cmake_build_args=${BUILD_ARGS//[$'\t\r\n']}" >> $GITHUB_OUTPUT
echo "cmake_test_args=${TEST_ARGS//[$'\t\r\n']}" >> $GITHUB_OUTPUT
- name: Install Ninja
if: ${{ inputs.generator == 'Ninja' }}
shell: bash
run: |
case "$OSTYPE" in
darwin*) brew install ninja ;;
linux*) sudo apt install -y ninja-build ;;
*) choco install ninja ;;
esac
- name: Configure the CMake project
shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}
run: cmake ${{ steps.process_inputs.outputs.cmake_args }}
- name: Build targets
shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}
run: cmake ${{ steps.process_inputs.outputs.cmake_build_args }}
- name: Run tests
if: steps.process_inputs.outputs.cmake_test_args != ''
shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}
run: ctest ${{ steps.process_inputs.outputs.cmake_test_args }}

193
dist/action.mjs 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}`));
}
});
});
}
/**
* 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.toString()));
}
return {
sourceDir,
buildDir: getInput("build-dir") || path.join(sourceDir, "build"),
configure: {
generator: getInput("generator"),
options,
args: parse(getInput("args")).map((arg) => arg.toString()),
},
build: {
enabled: getInput("run-build") == "true",
args: parse(getInput("build-args")).map((arg) => arg.toString()),
},
};
}
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,11 +0,0 @@
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,34 +0,0 @@
{
"name": "root",
"private": true,
"type": "module",
"scripts": {
"build": "rollup -c",
"format": "prettier --write --cache . !dist !README.md",
"lint": "eslint",
"test": "vitest"
},
"dependencies": {
"gha-utils": "^0.4.1"
},
"devDependencies": {
"@eslint/js": "^9.24.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.2",
"@tsconfig/node23": "^23.0.1",
"@types/node": "^22.14.1",
"@vitest/coverage-v8": "^3.0.8",
"eslint": "^9.24.0",
"prettier": "^3.5.3",
"rollup": "^4.40.0",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"typescript-eslint": "^8.30.1",
"vitest": "^3.0.8"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
}
}

File diff suppressed because it is too large Load Diff

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,152 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { buildProject, configureProject } from "./cmake.js";
import type { Context } from "./context.js";
import { exec } from "./exec.js";
interface TestCase {
name: string;
context?: Partial<Context>;
expectedArgs: string[];
}
const defaultContext: Context = {
sourceDir: "",
buildDir: "build",
configure: {
generator: "",
options: [],
args: [],
},
build: {
enabled: true,
args: [],
},
};
vi.mock("./exec.js", () => ({ exec: vi.fn() }));
describe("configure a CMake project", () => {
const testCases: TestCase[] = [
{
name: "with nothing specified",
expectedArgs: ["-B", "build"],
},
{
name: "with source directory specified",
context: { sourceDir: "project" },
expectedArgs: ["project", "-B", "build"],
},
{
name: "with build directory specified",
context: { buildDir: "output" },
expectedArgs: ["-B", "output"],
},
{
name: "with generator specified",
context: { configure: { generator: "Ninja", options: [], args: [] } },
expectedArgs: ["-B", "build", "-G", "Ninja"],
},
{
name: "with additional options specified",
context: {
configure: {
generator: "",
options: ["BUILD_TESTING=ON", "BUILD_EXAMPLES=ON"],
args: [],
},
},
expectedArgs: [
"-B",
"build",
"-DBUILD_TESTING=ON",
"-DBUILD_EXAMPLES=ON",
],
},
{
name: "with additional arguments specified",
context: {
configure: {
generator: "",
options: [],
args: ["-Wdev", "-Wdeprecated"],
},
},
expectedArgs: ["-B", "build", "-Wdev", "-Wdeprecated"],
},
{
name: "with all specified",
context: {
sourceDir: "project",
buildDir: "output",
configure: {
generator: "Ninja",
options: ["BUILD_TESTING=ON", "BUILD_EXAMPLES=ON"],
args: ["-Wdev", "-Wdeprecated"],
},
},
expectedArgs: [
"project",
"-B",
"output",
"-G",
"Ninja",
"-DBUILD_TESTING=ON",
"-DBUILD_EXAMPLES=ON",
"-Wdev",
"-Wdeprecated",
],
},
];
for (const testCase of testCases) {
it(`should execute the correct command ${testCase.name}`, async () => {
vi.mocked(exec).mockReset();
await configureProject({ ...defaultContext, ...testCase.context });
expect(exec).toHaveBeenCalledTimes(1);
expect(exec).toHaveBeenLastCalledWith("cmake", testCase.expectedArgs);
});
}
});
describe("build a CMake project", () => {
const testCases: TestCase[] = [
{
name: "with nothing specified",
expectedArgs: ["--build", "build"],
},
{
name: "with build directory specified",
context: { buildDir: "output" },
expectedArgs: ["--build", "output"],
},
{
name: "with additional arguments specified",
context: { build: { enabled: true, args: ["--target", "foo"] } },
expectedArgs: ["--build", "build", "--target", "foo"],
},
{
name: "with all specified",
context: {
buildDir: "output",
build: {
enabled: true,
args: ["--target", "foo"],
},
},
expectedArgs: ["--build", "output", "--target", "foo"],
},
];
for (const testCase of testCases) {
it(`should execute the correct command ${testCase.name}`, async () => {
vi.mocked(exec).mockReset();
await buildProject({ ...defaultContext, ...testCase.context });
expect(exec).toHaveBeenCalledTimes(1);
expect(exec).toHaveBeenLastCalledWith("cmake", testCase.expectedArgs);
});
}
});

View File

@ -1,43 +0,0 @@
import { exec } from "./exec.js";
import type { Context } from "./context.js";
/**
* 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.
*/
export async function configureProject(context: Context): Promise<void> {
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.
*/
export async function buildProject(context: Context): Promise<void> {
await exec("cmake", ["--build", context.buildDir, ...context.build.args]);
}

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.toString()));
}
return {
sourceDir,
buildDir: getInput("build-dir") || path.join(sourceDir, "build"),
configure: {
generator: getInput("generator"),
options,
args: parse(getInput("args")).map((arg) => arg.toString()),
},
build: {
enabled: getInput("run-build") == "true",
args: parse(getInput("build-args")).map((arg) => arg.toString()),
},
};
}

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) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Command exited with status code ${code}`));
}
});
});
}

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;
}

32
test/CMakeLists.txt Normal file
View File

@ -0,0 +1,32 @@
cmake_minimum_required(VERSION 3.0)
project(test)
option(CHECK_USING_CLANG "check if target is compiled using Clang" OFF)
option(CHECK_SURPASS_WARNING "check if target could surpass a compiler warning" OFF)
if(CHECK_SURPASS_WARNING)
if(MSVC)
set(CMAKE_C_FLAGS "/WX /W4 ${CMAKE_C_FLAGS}")
set(CMAKE_CXX_FLAGS "/WX /W4 ${CMAKE_CXX_FLAGS}")
else()
set(CMAKE_C_FLAGS "-Werror -Wunused-variable ${CMAKE_C_FLAGS}")
set(CMAKE_CXX_FLAGS "-Werror -Wunused-variable ${CMAKE_CXX_FLAGS}")
endif()
endif()
enable_testing()
add_executable(hello_world hello_world.cpp)
add_test(NAME hello_world COMMAND $<TARGET_FILE:hello_world>)
list(APPEND LANGS c cpp)
foreach(LANG ${LANGS})
configure_file(test.in ${CMAKE_CURRENT_BINARY_DIR}/test.${LANG})
add_executable(test_${LANG} EXCLUDE_FROM_ALL ${CMAKE_CURRENT_BINARY_DIR}/test.${LANG})
target_compile_definitions(
test_${LANG} PRIVATE
$<$<STREQUAL:"${LANG}","c">:IS_C>
$<$<BOOL:${CHECK_USING_CLANG}>:CHECK_USING_CLANG>
$<$<BOOL:${CHECK_SURPASS_WARNING}>:CHECK_SURPASS_WARNING>
)
add_test(NAME test_${LANG} COMMAND $<TARGET_FILE:test_${LANG}>)
endforeach()

6
test/hello_world.cpp Normal file
View File

@ -0,0 +1,6 @@
#include <iostream>
int main() {
std::cout << "Hello world!" << std::endl;
return 0;
}

19
test/test.in Normal file
View File

@ -0,0 +1,19 @@
#ifdef IS_C
#include <stdio.h>
#define PRINT(STR) printf(STR); printf("\n")
#else
#include <iostream>
#define PRINT(STR) std::cout << STR << std::endl
#endif
int main() {
#ifdef CHECK_SURPASS_WARNING
int unused;
#endif
#if defined(CHECK_USING_CLANG) && !defined(__clang__)
PRINT("compiler is not clang");
return 1;
#endif
PRINT("all ok");
return 0;
}

View File

@ -1,8 +0,0 @@
{
"extends": "@tsconfig/node23",
"include": ["src"],
"exclude": ["**/*.test.ts"],
"compilerOptions": {
"module": "node16"
}
}

View File

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