Skip to content

Commit 5225a3c

Browse files
committed
Support Linux and macOS
1 parent 760a110 commit 5225a3c

File tree

6 files changed

+177
-19
lines changed

6 files changed

+177
-19
lines changed

.eslintrc.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
"no-self-compare": "error",
2929
"no-sequences": "error",
3030
"no-useless-return": "error",
31-
"require-await": "error",
3231
"yoda": "error",
3332

3433
"brace-style": "error",

.github/workflows/main.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
name: CI
22

33
on:
4-
pull_request:
54
push:
6-
branches:
7-
- master
8-
- v1
5+
pull_request:
96
schedule:
107
- cron: '0 6 * * 6'
118

129
jobs:
1310
test:
14-
runs-on: windows-latest
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
os: [ubuntu-latest, macos-latest, windows-latest]
15+
runs-on: ${{ matrix.os }}
1516
steps:
1617
- uses: actions/checkout@v2
1718
- run: npm install

.github/workflows/release.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ on:
99

1010
jobs:
1111
test:
12-
runs-on: windows-latest
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
os: [ubuntu-latest, macos-latest, windows-latest]
16+
runs-on: ${{ matrix.os }}
1317
steps:
1418
- uses: oprypin/install-crystal@v1
1519
- run: crystal eval "puts 1337"

.util/sudo

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/sh
2+
3+
sleep 10

README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ install-crystal
33

44
[GitHub Action][] to **install [Crystal][] programming language**
55

6-
Currently works only on Windows, always downloads the latest "nightly" build.
6+
Works on Ubuntu, macOS, Windows.
77

88
## Examples
99

@@ -17,14 +17,30 @@ steps:
1717
1818
### Inputs
1919
20-
* **`token: ${{ github.token }}`**
20+
* **`crystal: 0.34.0`** (not supported on Windows)
2121

22-
Personal access token (auto-populated).
22+
Install a particular release of Crystal.
23+
24+
* **`crystal: latest`** (default; not supported on Windows)
25+
26+
Install the latest released version of Crystal.
27+
28+
* **`crystal: nightly`** (default on Windows; not supported on other systems)
29+
30+
Install Crystal from the latest continuous build on [crystal.git][] master.
31+
32+
* **`arch: x86_64`**, **`arch: x86`** (defaults to current OS arch)
33+
34+
The architecture of the build of Crystal to download.
2335

2436
* **`destination: some/path`**
2537

2638
The directory to store Crystal in, after extracting.
2739

40+
* **`token: ${{ github.token }}`**
41+
42+
Personal access token (auto-populated).
43+
2844
### Outputs
2945

3046
* **`crystal`** (`${{ steps.some_step_id.outputs.crystal }}`)

index.js

Lines changed: 144 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,149 @@ const FS = require("fs").promises;
88

99
async function run() {
1010
try {
11-
const path = await downloadCrystalForWindows();
12-
await setupCrystalForWindows(path);
11+
const params = {};
12+
for (const key of ["crystal", "arch", "destination"]) {
13+
let value;
14+
if ((value = Core.getInput(key))) {
15+
params[key] = value;
16+
}
17+
}
18+
const func = {
19+
[Linux]: installCrystalForLinux,
20+
[Mac]: installCrystalForMac,
21+
[Windows]: installCrystalForWindows,
22+
}[getPlatform()];
23+
if (!func) {
24+
throw `Platform "${getPlatform()}" is not supported`;
25+
}
26+
await func(params);
1327
} catch (error) {
1428
Core.setFailed(error);
29+
process.exit(1);
1530
}
1631
}
1732

33+
const Linux = "Linux", Mac = "macOS", Windows = "Windows";
34+
35+
function getPlatform() {
36+
const platform = process.env["INSTALL_CRYSTAL_PLATFORM"] || process.platform;
37+
return {"linux": Linux, "darwin": Mac, "win32": Windows}[platform] || platform;
38+
}
39+
40+
function getArch() {
41+
return {"ia32": "x86", "x64": "x86_64"}[process.arch] || process.arch;
42+
}
43+
44+
function checkArch(arch, allowed) {
45+
if (!allowed.includes(arch)) {
46+
throw `Architecture "${arch}" is not supported on ${getPlatform()}`;
47+
}
48+
}
49+
50+
const Latest = "latest";
51+
const Nightly = "nightly";
52+
const NumericVersion = /^\d[.\d]+\d$/;
53+
54+
function checkVersion(version, allowed) {
55+
const numericVersion = version.match(NumericVersion) && version;
56+
allowed[allowed.indexOf(NumericVersion)] = numericVersion;
57+
58+
if (allowed.includes(version)) {
59+
return version;
60+
}
61+
if ([Latest, Nightly, numericVersion].includes(version)) {
62+
throw `Version "${version}" is not supported on ${getPlatform()}`;
63+
}
64+
throw `Version "${version}" is invalid`;
65+
}
66+
67+
async function installCrystalForLinux({
68+
crystal = Latest,
69+
arch = getArch(),
70+
destination = null,
71+
}) {
72+
checkVersion(crystal, [Latest, NumericVersion]);
73+
const suffixes = {"x86_64": "linux-x86_64", "x86": "linux-i686"};
74+
checkArch(arch, Object.keys(suffixes));
75+
76+
return Promise.all([
77+
installAptPackages(
78+
"libevent-dev libgmp-dev libpcre3-dev libssl-dev libxml2-dev libyaml-dev".split(" "),
79+
),
80+
installBinaryRelease({crystal, suffix: suffixes[arch], destination}),
81+
]);
82+
}
83+
84+
async function installCrystalForMac({
85+
crystal = Latest,
86+
arch = "x86_64",
87+
destination = null,
88+
}) {
89+
checkVersion(crystal, [Latest, NumericVersion]);
90+
checkArch(arch, ["x86_64"]);
91+
return installBinaryRelease({crystal, suffix: "darwin-x86_64", destination});
92+
}
93+
94+
async function installAptPackages(packages) {
95+
const execFile = Util.promisify(ChildProcess.execFile);
96+
Core.info("Installing package dependencies");
97+
const args = [
98+
"-n", "apt-get", "install", "-qy", "--no-install-recommends", "--no-upgrade", "--",
99+
].concat(packages);
100+
Core.info("[command]sudo " + args.join(" "));
101+
const {stdout} = await execFile("sudo", args);
102+
Core.startGroup("Finished installing package dependencies");
103+
Core.info(stdout);
104+
Core.endGroup();
105+
}
106+
107+
async function installBinaryRelease({crystal, suffix, destination}) {
108+
if (crystal === Latest) {
109+
crystal = null;
110+
}
111+
const path = await downloadCrystalRelease({suffix, tag: crystal, destination});
112+
113+
Core.info("Setting up environment");
114+
Core.addPath(Path.join(path, "bin"));
115+
}
116+
117+
async function downloadCrystalRelease({suffix, tag = null, destination = null}) {
118+
Core.info("Looking for latest Crystal release");
119+
120+
const releasesResp = await githubGet({
121+
url: "/repos/crystal-lang/crystal/releases/" + (tag ? "tags/" + tag : "latest"),
122+
});
123+
const release = releasesResp.data;
124+
Core.info("Found " + release["html_url"]);
125+
Core.setOutput("crystal", release["tag_name"]);
126+
127+
const asset = release["assets"].find((a) => a["name"].endsWith([`-${suffix}.tar.gz`]));
128+
129+
Core.info("Downloading Crystal build");
130+
const downloadedPath = await ToolCache.downloadTool(asset["browser_download_url"]);
131+
132+
Core.info("Extracting Crystal build");
133+
return onlySubdir(await ToolCache.extractTar(downloadedPath, destination));
134+
}
135+
136+
async function installCrystalForWindows({
137+
crystal = Nightly,
138+
arch = "x86_64",
139+
destination = null,
140+
}) {
141+
checkVersion(crystal, [Nightly]);
142+
checkArch(arch, ["x86_64"]);
143+
const path = await downloadCrystalForWindows(destination);
144+
await setupCrystalForWindows(path);
145+
}
146+
18147
async function setupCrystalForWindows(path) {
19148
Core.info("Setting up environment");
20149
const vars = await variablesForVCBuildTools();
21150
addPathToVars(vars, "PATH", path);
22151
addPathToVars(vars, "LIB", path);
23152
addPathToVars(vars, "CRYSTAL_PATH", Path.join(path, "src"));
153+
addPathToVars(vars, "CRYSTAL_PATH", "lib");
24154
for (const [k, v] of vars.entries()) {
25155
Core.exportVariable(k, v);
26156
}
@@ -41,6 +171,7 @@ const vcvarsPath = String.raw`C:\Program Files (x86)\Microsoft Visual Studio\201
41171
async function variablesForVCBuildTools() {
42172
const exec = Util.promisify(ChildProcess.exec);
43173
const command = `set && echo ${outputSep} && "${vcvarsPath}" >nul && set`;
174+
Core.info(`[command]cmd /c "${command}"`);
44175
const {stdout} = await exec(command, {shell: "cmd"});
45176
Core.debug(JSON.stringify(stdout));
46177
return new Map(getChangedVars(stdout.trimEnd().split(/\r?\n/)));
@@ -66,7 +197,7 @@ function* getChangedVars(lines) {
66197
}
67198
}
68199

69-
async function downloadCrystalForWindows() {
200+
async function downloadCrystalForWindows(destination = null) {
70201
Core.info("Looking for latest Crystal build");
71202

72203
const runsResp = await githubGet({
@@ -91,9 +222,7 @@ async function downloadCrystalForWindows() {
91222
const downloadedPath = await ToolCache.downloadTool(downloadUrl);
92223

93224
Core.info("Extracting Crystal source");
94-
const extractedPath = await ToolCache.extractZip(downloadedPath, destDir);
95-
const [subDir] = await FS.readdir(extractedPath);
96-
return Path.join(extractedPath, subDir);
225+
return onlySubdir(await ToolCache.extractZip(downloadedPath, destDir));
97226
};
98227

99228
const fetchExeTask = async () => {
@@ -106,7 +235,6 @@ async function downloadCrystalForWindows() {
106235
const artifactLinkResp = await githubGet({
107236
url: "/repos/crystal-lang/crystal/actions/artifacts/:artifact_id/zip",
108237
"artifact_id": artifact.id,
109-
headers: {"authorization": "token " + Core.getInput("token")},
110238
request: {redirect: "manual"},
111239
});
112240
const downloadUrl = artifactLinkResp.headers["location"];
@@ -116,7 +244,7 @@ async function downloadCrystalForWindows() {
116244
};
117245

118246
const [srcPath, exeDownloadedPath] = await Promise.all([
119-
fetchSrcTask(Core.getInput("destination")),
247+
fetchSrcTask(destination),
120248
fetchExeTask(),
121249
]);
122250

@@ -126,7 +254,14 @@ async function downloadCrystalForWindows() {
126254

127255
function githubGet(request) {
128256
Core.debug(request);
129-
return Octokit.request(request);
257+
return Octokit.request.defaults({
258+
headers: {"authorization": "token " + Core.getInput("token")},
259+
})(request);
260+
}
261+
262+
async function onlySubdir(path) {
263+
const [subDir] = await FS.readdir(path);
264+
return Path.join(path, subDir);
130265
}
131266

132267
if (require.main === module) {

0 commit comments

Comments
 (0)