Skip to content

Commit 2a0f10e

Browse files
committed
Create setup-zig
0 parents  commit 2a0f10e

File tree

4,216 files changed

+696015
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

4,216 files changed

+696015
-0
lines changed

LICENSE

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Copyright Matthew Lugg
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8+

README.md

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# setup-zig
2+
3+
Install the Zig compiler for use in an Actions workflow, and preserve the Zig cache across workflow runs.
4+
5+
## Usage
6+
7+
```yaml
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
name: Build and Test
12+
steps:
13+
- uses: actions/checkout@v3
14+
- uses: mlugg/setup-zig@v1
15+
- run: zig build test
16+
```
17+
18+
This will automatically download Zig and install it to `PATH`.
19+
20+
You can use `version` to set a Zig version to download. This may be a release (`0.13.0`), a specific nightly
21+
build (`0.14.0-dev.2+0884a4341`), the string `master` for the latest nightly build, or the string `latest`
22+
for the latest full release. The default is `latest`.
23+
24+
```yaml
25+
- uses: mlugg/setup-zig@v1
26+
with:
27+
version: 0.13.0
28+
```
29+
30+
> [!WARNING]
31+
> Mirrors, including the official Zig website, may purge old nightly builds at their leisure. This means
32+
> that if you target an out-of-date nightly build, such as a `0.11.0-dev` build, the download may fail.
33+
34+
## Details
35+
36+
This action attempts to download the requested Zig tarball from a set of mirrors, in a random order. As
37+
a last resort, the official Zig website is used. The tarball's minisign signature is also downloaded and
38+
verified to ensure binaries have not been tampered with. The tarball is cached between runs and workflows.
39+
40+
The global Zig cache directory (`~/.cache/zig` on Linux) is automatically cached between runs, and all
41+
local caches are redirected to the global cache directory to make optimal use of this cross-run caching.

action.yml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: 'Setup Zig Compiler'
2+
description: 'Download and install the Zig compiler, and cache the global Zig cache'
3+
inputs:
4+
version:
5+
description: 'Version of the Zig compiler, e.g. "0.13.0" or "0.13.0-dev.351+64ef45eb0". "master" uses the latest nightly build. "latest" uses the latest tagged release.'
6+
required: true
7+
default: 'latest'
8+
runs:
9+
using: 'node20'
10+
main: 'main.js'
11+
post: 'post.js'

common.js

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
const os = require('os');
2+
const path = require('path');
3+
const core = require('@actions/core');
4+
const github = require('@actions/github');
5+
const exec = require('@actions/exec');
6+
7+
const VERSIONS_JSON = 'https://ziglang.org/download/index.json';
8+
const CACHE_PREFIX = "setup-zig-global-cache-";
9+
10+
let _cached_version = null;
11+
async function getVersion() {
12+
if (_cached_version != null) {
13+
return _cached_version;
14+
}
15+
16+
const raw = core.getInput('version');
17+
if (raw === 'master') {
18+
const resp = await fetch(VERSIONS_JSON);
19+
const versions = await resp.json();
20+
_cached_version = versions['master'].version;
21+
} else if (raw === 'latest') {
22+
const resp = await fetch(VERSIONS_JSON);
23+
const versions = await resp.json();
24+
let latest = null;
25+
let latest_major;
26+
let latest_minor;
27+
let latest_patch;
28+
for (const version in versions) {
29+
if (version === 'master') continue;
30+
const [major_str, minor_str, patch_str] = version.split('.')
31+
const major = Number(major_str);
32+
const minor = Number(minor_str);
33+
const patch = Number(patch_str);
34+
if (latest === null) {
35+
latest = version;
36+
latest_major = major;
37+
latest_minor = minor;
38+
latest_patch = patch;
39+
continue;
40+
}
41+
if (major > latest_major ||
42+
(major == latest_major && minor > latest_minor) ||
43+
(major == latest_major && minor == latest_minor && patch > latest_patch))
44+
{
45+
latest = version;
46+
latest_major = major;
47+
latest_minor = minor;
48+
latest_patch = patch;
49+
}
50+
}
51+
_cached_version = latest;
52+
} else {
53+
_cached_version = raw;
54+
}
55+
56+
return _cached_version;
57+
}
58+
59+
async function getTarballName() {
60+
const version = await getVersion();
61+
62+
const arch = {
63+
arm: 'armv7a',
64+
arm64: 'aarch64',
65+
ppc64: 'powerpc64',
66+
riscv64: 'riscv64',
67+
x64: 'x86_64',
68+
}[os.arch()];
69+
70+
return {
71+
linux: `zig-linux-${arch}-${version}`,
72+
darwin: `zig-macos-${arch}-${version}`,
73+
win32: `zig-windows-${arch}-${version}`,
74+
}[os.platform()];
75+
}
76+
77+
async function getTarballExt() {
78+
return {
79+
linux: '.tar.xz',
80+
darwin: '.tar.xz',
81+
win32: '.zip',
82+
}[os.platform()];
83+
}
84+
85+
async function getCachePrefix() {
86+
const tarball_name = await getTarballName();
87+
const job_name = github.context.job.replaceAll(/[^\w]/g, "_");
88+
return `setup-zig-cache-${job_name}-${tarball_name}-`;
89+
}
90+
91+
async function getZigCachePath() {
92+
let env_output = '';
93+
await exec.exec('zig', ['env'], {
94+
listeners: {
95+
stdout: (data) => {
96+
env_output += data.toString();
97+
},
98+
},
99+
});
100+
return JSON.parse(env_output)['global_cache_dir'];
101+
}
102+
103+
async function getTarballCachePath() {
104+
return path.join(process.env['RUNNER_TEMP'], await getTarballName());
105+
}
106+
107+
module.exports = {
108+
getVersion,
109+
getTarballName,
110+
getTarballExt,
111+
getCachePrefix,
112+
getZigCachePath,
113+
getTarballCachePath,
114+
};

main.js

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
const os = require('os');
2+
const path = require('path');
3+
const fs = require('fs').promises;
4+
const core = require('@actions/core');
5+
const tc = require('@actions/tool-cache');
6+
const cache = require('@actions/cache');
7+
const common = require('./common');
8+
const minisign = require('./minisign');
9+
10+
// Upstream's minisign key, from https://ziglang.org/download
11+
const MINISIGN_KEY = 'RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U';
12+
13+
// The base URL of the official builds of Zig. This is only used as a fallback, if all mirrors fail.
14+
const CANONICAL = 'https://ziglang.org/builds';
15+
16+
// The list of mirrors we attempt to fetch from. These need not be trusted, as
17+
// we always verify the minisign signature.
18+
const MIRRORS = [
19+
// TODO: are there any more mirrors around?
20+
'https://pkg.machengine.org/zig',
21+
];
22+
23+
async function downloadFromMirror(mirror, tarball_name, tarball_ext) {
24+
const tarball_path = await tc.downloadTool(`${mirror}/${tarball_name}${tarball_ext}`);
25+
26+
const signature_response = await fetch(`${mirror}/${tarball_name}${tarball_ext}.minisig`);
27+
const signature_data = Buffer.from(await signature_response.arrayBuffer());
28+
29+
const tarball_data = await fs.readFile(tarball_path);
30+
31+
const key = minisign.parseKey(MINISIGN_KEY);
32+
const signature = minisign.parseSignature(signature_data);
33+
if (!minisign.verifySignature(key, signature, tarball_data)) {
34+
throw new Error(`signature verification failed for '${mirror}/${tarball_name}${tarball_ext}'`);
35+
}
36+
37+
return tarball_path;
38+
}
39+
40+
async function downloadTarball(tarball_name, tarball_ext) {
41+
// We will attempt all mirrors before making a last-ditch attempt to the official download.
42+
// To avoid hammering a single mirror, we first randomize the array.
43+
const shuffled_mirrors = MIRRORS.map((m) => [m, Math.random()]).sort((a, b) => a[1] - b[1]).map((a) => a[0]);
44+
for (const mirror of shuffled_mirrors) {
45+
core.info(`Attempting mirror: ${mirror}`);
46+
try {
47+
return await downloadFromMirror(mirror, tarball_name, tarball_ext);
48+
} catch (e) {
49+
core.info(`Mirror failed with error: ${e}`);
50+
// continue loop to next mirror
51+
}
52+
}
53+
core.info(`Attempting official: ${CANONICAL}`);
54+
return await downloadFromMirror(CANONICAL, tarball_name, tarball_ext);
55+
}
56+
57+
async function retrieveTarball(tarball_name, tarball_ext) {
58+
const cache_key = `setup-zig-tarball-${tarball_name}`;
59+
const tarball_cache_path = await common.getTarballCachePath();
60+
61+
if (await cache.restoreCache([tarball_cache_path], cache_key)) {
62+
return tarball_cache_path;
63+
}
64+
65+
core.info(`Cache miss. Fetching Zig ${await common.getVersion()}`);
66+
const downloaded_path = await downloadTarball(tarball_name, tarball_ext);
67+
await fs.copyFile(downloaded_path, tarball_cache_path)
68+
await cache.saveCache([tarball_cache_path], cache_key);
69+
return tarball_cache_path;
70+
}
71+
72+
async function main() {
73+
try {
74+
// We will check whether Zig is stored in the cache. We use two separate caches.
75+
// * 'tool-cache' caches the final extracted directory if the same Zig build is used multiple
76+
// times by one job. We have this dependency anyway for archive extraction.
77+
// * 'cache' only caches the unextracted archive, but it does so across runs. It's a little
78+
// less efficient, but still much preferable to fetching Zig from a mirror. We have this
79+
// dependency anyway for caching the global Zig cache.
80+
81+
let zig_dir = tc.find('zig', await common.getVersion());
82+
if (!zig_dir) {
83+
const tarball_name = await common.getTarballName();
84+
const tarball_ext = await common.getTarballExt();
85+
86+
const tarball_path = await retrieveTarball(tarball_name, tarball_ext);
87+
88+
core.info(`Extracting tarball ${tarball_name}${tarball_ext}`);
89+
90+
const zig_parent_dir = tarball_ext === 'zip' ?
91+
await tc.extractZip(tarball_path) :
92+
await tc.extractTar(tarball_path, null, 'xJ'); // J for xz
93+
94+
const zig_inner_dir = path.join(zig_parent_dir, tarball_name);
95+
zig_dir = await tc.cacheDir(zig_inner_dir, 'zig', await common.getVersion());
96+
}
97+
98+
core.addPath(zig_dir);
99+
await cache.restoreCache([await common.getZigCachePath()], await common.getCachePrefix());
100+
// Direct Zig to use the global cache as every local cache, so that we get maximum benefit from the caching above.
101+
core.exportVariable('ZIG_LOCAL_CACHE_DIR', await common.getZigCachePath());
102+
} catch (err) {
103+
core.setFailed(err.message);
104+
}
105+
}
106+
107+
main();

minisign.js

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
const sodium = require('sodium-native');
2+
3+
// Parse a minisign key represented as a base64 string.
4+
// Throws exceptions on invalid keys.
5+
function parseKey(key_str) {
6+
const key_info = Buffer.from(key_str, 'base64');
7+
8+
const id = key_info.subarray(2, 10);
9+
const key = key_info.subarray(10);
10+
11+
if (key.byteLength !== sodium.crypto_sign_PUBLICKEYBYTES) {
12+
throw new Error('invalid public key given');
13+
}
14+
15+
return {
16+
id: id,
17+
key: key
18+
};
19+
}
20+
21+
// Parse a buffer containing the contents of a minisign signature file.
22+
// Throws exceptions on invalid signature files.
23+
function parseSignature(sig_buf) {
24+
const untrusted_header = Buffer.from('untrusted comment: ');
25+
26+
// Validate untrusted comment header, and skip
27+
if (!sig_buf.subarray(0, untrusted_header.byteLength).equals(untrusted_header)) {
28+
throw new Error('file format not recognised');
29+
}
30+
sig_buf = sig_buf.subarray(untrusted_header.byteLength);
31+
32+
// Skip untrusted comment
33+
sig_buf = sig_buf.subarray(sig_buf.indexOf('\n') + 1);
34+
35+
// Read and skip signature info
36+
const sig_info_end = sig_buf.indexOf('\n');
37+
const sig_info = Buffer.from(sig_buf.subarray(0, sig_info_end).toString(), 'base64');
38+
sig_buf = sig_buf.subarray(sig_info_end + 1);
39+
40+
// Extract components of signature info
41+
const algorithm = sig_info.subarray(0, 2);
42+
const key_id = sig_info.subarray(2, 10);
43+
const signature = sig_info.subarray(10);
44+
45+
// We don't look at the trusted comment or global signature, so we're done.
46+
47+
return {
48+
algorithm: algorithm,
49+
key_id: key_id,
50+
signature: signature,
51+
};
52+
}
53+
54+
// Given a parsed key, parsed signature file, and raw file content, verifies the
55+
// signature. Does not throw. Returns 'true' if the signature is valid for this
56+
// file, 'false' otherwise.
57+
function verifySignature(pubkey, signature, file_content) {
58+
let signed_content;
59+
if (signature.algorithm.equals(Buffer.from('ED'))) {
60+
signed_content = Buffer.alloc(sodium.crypto_generichash_BYTES_MAX);
61+
sodium.crypto_generichash(signed_content, file_content);
62+
} else {
63+
signed_content = file_content;
64+
}
65+
66+
if (!signature.key_id.equals(pubkey.id)) {
67+
return false;
68+
}
69+
70+
if (!sodium.crypto_sign_verify_detached(signature.signature, signed_content, pubkey.key)) {
71+
return false;
72+
}
73+
74+
// Since we don't use the trusted comment, we don't bother verifying the global signature.
75+
// If we were to start using the trusted comment for any purpose, we must add this.
76+
77+
return true;
78+
}
79+
80+
module.exports = {
81+
parseKey,
82+
parseSignature,
83+
verifySignature,
84+
};

node_modules/.bin/fxparser

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

node_modules/.bin/node-gyp-build

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

node_modules/.bin/node-gyp-build-optional

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

node_modules/.bin/node-gyp-build-test

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

node_modules/.bin/semver

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

node_modules/.bin/uuid

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)