Skip to content

Commit

Permalink
Local cache: #66
Browse files Browse the repository at this point in the history
  • Loading branch information
lukka committed Dec 18, 2022
1 parent 167249a commit 28ce1c8
Show file tree
Hide file tree
Showing 11 changed files with 357 additions and 107 deletions.
40 changes: 30 additions & 10 deletions README.md
Expand Up @@ -20,33 +20,53 @@
<br>

# [The **get-cmake** action installs as fast as possible your desired versions of CMake and Ninja](https://github.com/marketplace/actions/get-cmake)
The action restores from the GitHub cloud based cache, or downloads and caches, both CMake and Ninja. You can select your desired version using [semantic versioning ranges](https://docs.npmjs.com/about-semantic-versioning), and also use `install` or `installrc` special versions to install the [latest stable](./.latest_cmake_version) or [release candidate](./.latest_ninja_version).
The action restores from local or cloud based cache both CMake and Ninja. If a `cache-miss` occurs, it downloads and caches the tools right away.

Works for `x64` and `arm64` hosts on Linux, macOS and Windows.

The desired version can be specified using [semantic versioning ranges](https://docs.npmjs.com/about-semantic-versioning), and also use `install` or `installrc` special tokens to install resp. the [latest stable](./.latest_cmake_version) or [release candidate](./.latest_ninja_version).

There are two kind of caches:
- The cloud based [GitHub cache](https://www.npmjs.com/package/@actions/cache). Enabled by default, it can be disabled using the input `useCloudCache:false`.
- The local self-hosted runner cache, stored locally using [tool-cache](https://www.npmjs.com/package/@actions/tool-cache). Disabled by default, it can enabled with the input `useLocalCache:true`.


Steps of `get-cmake`:
1. If a cache hit occurs, CMake and Ninja are restored from cache in less than 1 second.
2. If a cache miss occurs:
1. If a `cache-hit` occurs (either local or cloud cache), CMake and Ninja are restored from the cache.
1. if both local and cloud are enabled, the local cache check goes first.
2. If none of the enabled caches hits (`cache-miss`):
1. the action downloads and installs the desired versions of CMake and Ninja.
2. then it pushes both CMake and Ninja on the [cloud based GitHub cache](https://www.npmjs.com/package/@actions/cache). This is beneficial for the next run of the workflow.
3. Adds to the PATH environment variable the paths to CMake and Ninja executables.
2. the action stores CMake and Ninja in the enabled caches:
1. on the [cloud based GitHub cache](https://www.npmjs.com/package/@actions/cache). This is beneficial for the next run of the workflow especially on _GitHub-hosted runners_.
2. on the local GitHub runner cache. This is beneficial for the next run of the workflow on the same _self-hosted runner_.

_Note:_ when there is a `cache-hit`, nothing will be stored in any of the caches.
3. Adds to the `PATH` environment variable the binary directories for CMake and Ninja.

<br>

## Quickstart
### If you want to use **latest stable** you can use this one-liner:
```yaml
# Option 1: using 'latest' branch, the most recent CMake and ninja are installed.
- uses: lukka/get-cmake@latest # <--= Just this one-liner suffices.
- uses: lukka/get-cmake@latest # <--= Just this one-liner suffices.
```
or there is another option:
The local and cloud cache can be enabled or disabled, for example:
```yaml
# Suited for self-hosted GH runners where the local cache wins over the cloud.
- uses: lukka/get-cmake@latest
with:
useLocalCache: true # <--= Use the local cache (default is 'false').
useCloudCache: false # <--= Ditch the cloud cache (default is 'true').
```
And there is a second option:
```yaml
# Option 2: specify 'latest' or 'latestrc' in the input version arguments:
- name: Get latest CMake and Ninja
uses: lukka/get-cmake@latest
with:
cmakeVersion: latestrc # <--= optional, use the latest release candidate (notice the 'rc' suffix).
ninjaVersion: latest # <--= optional, use the latest release (non candidate).
cmakeVersion: latestrc # <--= optional, use the latest release candidate (notice the 'rc' suffix).
ninjaVersion: latest # <--= optional, use the latest release (non candidate).
```

<br>
Expand All @@ -58,7 +78,7 @@ or there is another option:
with:
cmakeVersion: "~3.25.0" # <--= optional, use most recent 3.25.x version
ninjaVersion: "^1.11.1" # <--= optional, use most recent 1.x version

# or using a specific version (no range)
- uses: lukka/get-cmake@latest
with:
Expand Down
105 changes: 105 additions & 0 deletions __tests__/action_succeeded.localcloud.test.ts
@@ -0,0 +1,105 @@
// Copyright (c) 2022 Luca Cappa
// Released under the term specified in file LICENSE.txt
// SPDX short identifier: MIT

import * as os from 'os';
import * as toolcache from '@actions/tool-cache';
import * as core from '@actions/core';
import * as path from 'path'
import { main, ToolsGetter } from '../src/get-cmake';

// 30 minutes
jest.setTimeout(30 * 60 * 1000)

const localCacheInput = "__TEST__USE_LOCAL_CACHE";
const cloudCacheInput = "__TEST__USE_CLOUD_CACHE";
const localCacheHit = "__TEST__LOCAL_CACHE_HIT";
const cloudCacheHit = "__TEST__CLOUD_CACHE_HIT";

let restoreCache = jest.spyOn(ToolsGetter.prototype as any, 'restoreCache');
let saveCache = jest.spyOn(ToolsGetter.prototype as any, 'saveCache');

jest.spyOn(core, 'getInput').mockImplementation((arg: string, options: core.InputOptions | undefined): string => {
if (arg === "cmakeVersion")
return process.env["CUSTOM_CMAKE_VERSION"] || "";
else
return "";
});

jest.spyOn(core, 'getBooleanInput').mockImplementation((arg: string, options: core.InputOptions | undefined): boolean => {
switch (arg) {
case "useLocalCache":
return process.env[localCacheInput] === "true";
case "useCloudCache":
return process.env[cloudCacheInput] === "true";
default:
return false;
}
});

var coreSetFailed = jest.spyOn(core, 'setFailed');
var coreError = jest.spyOn(core, 'error');
var toolsCacheDir = jest.spyOn(toolcache, 'cacheDir');
var toolsFind = jest.spyOn(toolcache, 'find');

test('testing get-cmake action success with cloud/local cache enabled', async () => {
for (var matrix of [
{ version: "latest", cloudCache: "true", localCache: "true" },
{ version: "latest", cloudCache: "true", localCache: "false" },
{ version: "latest", cloudCache: "false", localCache: "true" },
{ version: "latest", cloudCache: "false", localCache: "false" }]) {

process.env.RUNNER_TEMP = path.join(os.tmpdir(), `${process.pid}`);
process.env.RUNNER_TOOL_CACHE = path.join(os.tmpdir(), `${process.pid}-cache`);
process.env["CUSTOM_CMAKE_VERSION"] = matrix.version;
process.env[localCacheInput] = matrix.localCache;
process.env[cloudCacheInput] = matrix.cloudCache;
await main();
expect(coreSetFailed).toBeCalledTimes(0);
expect(coreError).toBeCalledTimes(0);
expect(toolsCacheDir).toBeCalledTimes(matrix.localCache === "true" ? 1 : 0);
expect(toolsFind).toBeCalledTimes(matrix.localCache === "true" ? 1 : 0);
expect(saveCache).toBeCalledTimes(matrix.cloudCache === "true" ? 1 : 0);
expect(restoreCache).toBeCalledTimes(matrix.cloudCache === "true" ? 1 : 0);

saveCache.mockReset();
restoreCache.mockReset();
toolsCacheDir.mockReset();
toolsFind.mockReset();
}
});

test('testing get-cmake action success with local or cloud cache hits', async () => {
for (var matrix of [
{ version: "latest", cloudCache: true, localCache: true, localHit: false, cloudHit: true },
{ version: "latest", cloudCache: false, localCache: true, localHit: false, cloudHit: false },
{ version: "latest", cloudCache: true, localCache: true, localHit: true, cloudHit: false },
{ version: "latest", cloudCache: false, localCache: true, localHit: true, cloudHit: false },
]) {
saveCache.mockReset().mockResolvedValue(0);
restoreCache.mockReset().mockImplementation(
async () => {
return Promise.resolve(process.env[cloudCacheHit] === 'true' ? "hit" : "");
});
toolsCacheDir.mockReset().mockResolvedValue("mock");
toolsFind.mockReset().mockImplementation((toolName: string, versionSpec: string, arch?: string | undefined): string => {
return process.env[localCacheHit] === 'true' ? "hit" : "";
});

console.log(`\n\ntesting for: ${JSON.stringify(matrix)}:\n`)
process.env.RUNNER_TEMP = path.join(os.tmpdir(), `${process.pid}`);
process.env.RUNNER_TOOL_CACHE = path.join(os.tmpdir(), `${process.pid}-cache`);
process.env["CUSTOM_CMAKE_VERSION"] = matrix.version;
process.env[localCacheInput] = String(matrix.localCache);
process.env[cloudCacheInput] = String(matrix.cloudCache);
process.env[localCacheHit] = String(matrix.localHit);
process.env[cloudCacheHit] = String(matrix.cloudHit);
await main();
expect(coreSetFailed).toBeCalledTimes(0);
expect(coreError).toBeCalledTimes(0);
expect(toolsFind).toBeCalledTimes(matrix.localCache ? 1 : 0);
expect(toolsCacheDir).toBeCalledTimes(!matrix.localCache || matrix.localHit ? 0 : 1);
expect(saveCache).toBeCalledTimes((matrix.cloudHit || !matrix.cloudCache || matrix.localHit) ? 0 : 1);
expect(restoreCache).toBeCalledTimes((matrix.localHit || !matrix.cloudCache) ? 0 : 1);
}
});
21 changes: 18 additions & 3 deletions __tests__/action_succeeded.test.ts
@@ -1,4 +1,5 @@
// Copyright (c) 2020 Luca Cappa
// Copyright (c) 2020, 2021, 2022 Luca Cappa

// Released under the term specified in file LICENSE.txt
// SPDX short identifier: MIT

Expand All @@ -12,6 +13,10 @@ import { InputOptions } from '@actions/core';
// 30 minutes
jest.setTimeout(30 * 60 * 1000)

const localCacheInput = "__TEST__USE_LOCAL_CACHE";
const cloudCacheInput = "__TEST__USE_CLOUD_CACHE";


jest.spyOn(cache, 'saveCache').mockImplementation(() =>
Promise.resolve(0)
);
Expand All @@ -27,9 +32,19 @@ jest.spyOn(core, 'getInput').mockImplementation((arg: string, options: InputOpti
return "";
});

jest.spyOn(core, 'getBooleanInput').mockImplementation((arg: string, options: InputOptions | undefined): boolean => {
switch (arg) {
case "useLocalCache":
return process.env["localCacheInput"] === "true";
case "useCloudCache":
return process.env["cloudCacheInput"] === "true";
default:
return false;
}
});

var coreSetFailed = jest.spyOn(core, 'setFailed');
var coreError = jest.spyOn(core, 'error');
var toolsCacheDir = jest.spyOn(toolcache, 'cacheDir');

test('testing get-cmake action success with default cmake', async () => {
process.env.RUNNER_TEMP = os.tmpdir();
Expand All @@ -50,7 +65,7 @@ test('testing get-cmake action success with specific cmake versions', async () =
}

// A Linux ARM build is not available before 3.19.x
if (process.platform === "linux" && process.arch === "x64" ) {
if (process.platform === "linux" && process.arch === "x64") {
for (var version of ["3.18.3", "3.16.1", "3.5.2", "3.3.0", "3.1.2"]) {
process.env.RUNNER_TEMP = os.tmpdir();
process.env["CUSTOM_CMAKE_VERSION"] = version;
Expand Down
3 changes: 2 additions & 1 deletion __tests__/cachehit.test.ts
@@ -1,4 +1,5 @@
// Copyright (c) 2020 Luca Cappa
// Copyright (c) 2020, 2021, 2022 Luca Cappa

// Released under the term specified in file LICENSE.txt
// SPDX short identifier: MIT

Expand Down
3 changes: 2 additions & 1 deletion __tests__/cachemiss.test.ts
@@ -1,4 +1,5 @@
// Copyright (c) 2020 Luca Cappa
// Copyright (c) 2020, 2021, 2022 Luca Cappa

// Released under the term specified in file LICENSE.txt
// SPDX short identifier: MIT

Expand Down
6 changes: 3 additions & 3 deletions __tests__/cacherestorefailure.test.ts
@@ -1,4 +1,5 @@
// Copyright (c) 2020 Luca Cappa
// Copyright (c) 2020, 2021, 2022 Luca Cappa

// Released under the term specified in file LICENSE.txt
// SPDX short identifier: MIT

Expand All @@ -16,8 +17,7 @@ jest.spyOn(cache, 'saveCache').mockImplementation(() =>

jest.spyOn(cache, 'restoreCache').mockImplementation(() => {
throw new Error();
}
);
});

test('testing get-cmake with restoreCache failure', async () => {
process.env.RUNNER_TEMP = os.tmpdir();
Expand Down
3 changes: 2 additions & 1 deletion __tests__/notmpdir.test.ts
@@ -1,4 +1,5 @@
// Copyright (c) 2020 Luca Cappa
// Copyright (c) 2020, 2021, 2022 Luca Cappa

// Released under the term specified in file LICENSE.txt
// SPDX short identifier: MIT

Expand Down
10 changes: 9 additions & 1 deletion action.yml
Expand Up @@ -3,7 +3,7 @@
# SPDX short identifier: MIT

name: 'get-cmake'
description: 'Installs (and caches on GitHub cloud cache) CMake and Ninja onto GitHub runners.'
description: 'Installs CMake and Ninja, and caches them on cloud based GitHub cache, and/or on the local GitHub runner cache.'
author: 'Luca Cappa https://github.com/lukka'
runs:
using: 'node16'
Expand All @@ -16,6 +16,14 @@ inputs:
ninjaVersion:
required: false
description: "Optional Ninja version, same syntax as `cmakeVersion` input. If not specified, `latest` is installed"
useCloudCache:
required: false
description: "Optional argument indicating whether to use the cloud based storage of the GitHub cache. Suited for the GitHub-hosted runners."
default: true
useLocalCache:
required: false
description: "Optional argument indicating whether to use the local cache on the GitHub runner file system. Suited for the self-hosted GitHub runners."
default: false

branding:
icon: 'terminal'
Expand Down

0 comments on commit 28ce1c8

Please sign in to comment.