Skip to content

Commit 083e0f7

Browse files
authored
feat(tools/haskell): add cabal and ghc installer (#6099)
1 parent 9919f93 commit 083e0f7

14 files changed

Lines changed: 378 additions & 1 deletion

File tree

.github/workflows/build.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ jobs:
324324
- flutter
325325
- flux
326326
- golang
327+
- haskell
327328
- java
328329
- jb
329330
- helm
@@ -343,6 +344,13 @@ jobs:
343344
- name: aarch64
344345
os: ubuntu-24.04-arm
345346
tag: arm64
347+
# include:
348+
# # no ubuntu arm64 support for haskell
349+
# - lang: haskell
350+
# arch:
351+
# name: x86_64
352+
# os: ubuntu-24.04
353+
# tag: amd64
346354

347355
env:
348356
TAG: ${{ matrix.lang }}

docs/custom-registries.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,24 @@ https://dl.google.com/go/go1.21.6.linux-arm64.tar.gz
255255
https://dl.google.com/go/go1.17.5.linux-amd64.tar.gz
256256
```
257257

258+
## `haskell`
259+
260+
Haskell tools are separated.
261+
262+
### `cabal`
263+
264+
Cabal releases are downloaded from:
265+
266+
- `https://downloads.haskell.org/~cabal/`
267+
268+
Samples:
269+
270+
```txt
271+
https://downloads.haskell.org/~cabal/cabal-install-3.16.1.0/cabal-install-3.16.1.0-aarch64-linux-deb10.tar.xz
272+
https://downloads.haskell.org/~cabal/cabal-install-3.16.1.0/cabal-install-3.16.1.0-x86_64-linux-deb10.tar.xz
273+
https://downloads.haskell.org/~cabal/cabal-install-3.16.1.0/SHA256SUMS
274+
```
275+
258276
## `helm`
259277

260278
Helm releases are downloaded from:

src/cli/install-tool/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import { FluxInstallService } from '../tools/flux';
2626
import { GitLfsInstallService } from '../tools/git/lfs';
2727
import { GleamInstallService } from '../tools/gleam';
2828
import { GolangInstallService } from '../tools/golang';
29+
import { CabalInstallService } from '../tools/haskell/cabal';
30+
import { GhcInstallService } from '../tools/haskell/ghc';
2931
import { HelmInstallService } from '../tools/helm';
3032
import { HelmfileInstallService } from '../tools/helmfile';
3133
import {
@@ -121,6 +123,7 @@ async function prepareInstallContainer(): Promise<Container> {
121123
container.bind(INSTALL_TOOL_TOKEN).to(BazeliskInstallService);
122124
container.bind(INSTALL_TOOL_TOKEN).to(BuildxInstallService);
123125
container.bind(INSTALL_TOOL_TOKEN).to(BunInstallService);
126+
container.bind(INSTALL_TOOL_TOKEN).to(CabalInstallService);
124127
container.bind(INSTALL_TOOL_TOKEN).to(CocoapodsInstallService);
125128
container.bind(INSTALL_TOOL_TOKEN).to(ConanInstallService);
126129
container.bind(INSTALL_TOOL_TOKEN).to(DartInstallService);
@@ -134,6 +137,7 @@ async function prepareInstallContainer(): Promise<Container> {
134137
container.bind(INSTALL_TOOL_TOKEN).to(FlutterInstallService);
135138
container.bind(INSTALL_TOOL_TOKEN).to(FluxInstallService);
136139
container.bind(INSTALL_TOOL_TOKEN).to(GitLfsInstallService);
140+
container.bind(INSTALL_TOOL_TOKEN).to(GhcInstallService);
137141
container.bind(INSTALL_TOOL_TOKEN).to(GleamInstallService);
138142
container.bind(INSTALL_TOOL_TOKEN).to(GolangInstallService);
139143
container.bind(INSTALL_TOOL_TOKEN).to(GradleInstallService);

src/cli/prepare-tool/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { ErlangPrepareService } from '../tools/erlang';
77
import { ElixirPrepareService } from '../tools/erlang/elixir';
88
import { FlutterPrepareService } from '../tools/flutter';
99
import { GolangPrepareService } from '../tools/golang';
10+
import { CabalPrepareService } from '../tools/haskell/cabal';
11+
import { GhcPrepareService } from '../tools/haskell/ghc';
1012
import {
1113
JavaJdkPrepareService,
1214
JavaJrePrepareService,
@@ -47,13 +49,15 @@ async function prepareContainer(): Promise<Container> {
4749
}
4850

4951
// modern tool services
52+
container.bind(PREPARE_TOOL_TOKEN).to(CabalPrepareService);
5053
container.bind(PREPARE_TOOL_TOKEN).to(ConanPrepareService);
5154
container.bind(PREPARE_TOOL_TOKEN).to(DartPrepareService);
5255
container.bind(PREPARE_TOOL_TOKEN).to(DotnetPrepareService);
5356
container.bind(PREPARE_TOOL_TOKEN).to(DockerPrepareService);
5457
container.bind(PREPARE_TOOL_TOKEN).to(ElixirPrepareService);
5558
container.bind(PREPARE_TOOL_TOKEN).to(ErlangPrepareService);
5659
container.bind(PREPARE_TOOL_TOKEN).to(FlutterPrepareService);
60+
container.bind(PREPARE_TOOL_TOKEN).to(GhcPrepareService);
5761
container.bind(PREPARE_TOOL_TOKEN).to(GolangPrepareService);
5862
container.bind(PREPARE_TOOL_TOKEN).to(JavaPrepareService);
5963
container.bind(PREPARE_TOOL_TOKEN).to(JavaJrePrepareService);

src/cli/tools/haskell/cabal.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import fs from 'node:fs/promises';
2+
import { join } from 'node:path';
3+
import { injectFromHierarchy, injectable } from 'inversify';
4+
import { BaseInstallService } from '../../install-tool/base-install.service';
5+
import { BasePrepareService } from '../../prepare-tool/base-prepare.service';
6+
import { isFourPartVersion } from '../../utils';
7+
8+
@injectable()
9+
@injectFromHierarchy()
10+
export class CabalPrepareService extends BasePrepareService {
11+
readonly name = 'cabal';
12+
}
13+
14+
@injectable()
15+
@injectFromHierarchy()
16+
export class CabalInstallService extends BaseInstallService {
17+
readonly name = 'cabal';
18+
19+
private get arch(): string {
20+
switch (this.envSvc.arch) {
21+
case 'arm64':
22+
return 'aarch64';
23+
case 'amd64':
24+
return 'x86_64';
25+
}
26+
}
27+
28+
override async install(version: string): Promise<void> {
29+
const baseUrl = `https://downloads.haskell.org/~cabal/cabal-install-${version}/`;
30+
// use static deb10 binary as it is compatible with all supported ubuntu versions
31+
const filename = `cabal-install-${version}-${this.arch}-linux-deb10.tar.xz`;
32+
33+
const checksumFile = await this.http.download({
34+
url: `${baseUrl}SHA256SUMS`,
35+
});
36+
const expectedChecksum = (await fs.readFile(checksumFile, 'utf-8'))
37+
.split('\n')
38+
.find((l) => l.includes(filename))
39+
?.split(' ')[0];
40+
41+
const file = await this.http.download({
42+
url: `${baseUrl}${filename}`,
43+
checksumType: 'sha256',
44+
expectedChecksum,
45+
});
46+
47+
await this.pathSvc.ensureToolPath(this.name);
48+
49+
const path = join(
50+
await this.pathSvc.createVersionedToolPath(this.name, version),
51+
'bin',
52+
);
53+
await fs.mkdir(path);
54+
await this.compress.extract({
55+
file,
56+
cwd: path,
57+
});
58+
}
59+
60+
override validate(version: string): Promise<boolean> {
61+
return Promise.resolve(isFourPartVersion(version));
62+
}
63+
64+
override async link(version: string): Promise<void> {
65+
const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin');
66+
67+
await this.shellwrapper({ srcDir: src });
68+
}
69+
70+
override async test(_version: string): Promise<void> {
71+
await this._spawn('cabal', ['--version']);
72+
}
73+
}

src/cli/tools/haskell/ghc.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import fs from 'node:fs/promises';
2+
import { join } from 'node:path';
3+
import { injectFromHierarchy, injectable } from 'inversify';
4+
import { BaseInstallService } from '../../install-tool/base-install.service';
5+
import { BasePrepareService } from '../../prepare-tool/base-prepare.service';
6+
7+
@injectable()
8+
@injectFromHierarchy()
9+
export class GhcPrepareService extends BasePrepareService {
10+
readonly name = 'ghc';
11+
}
12+
13+
@injectable()
14+
@injectFromHierarchy()
15+
export class GhcInstallService extends BaseInstallService {
16+
readonly name = 'ghc';
17+
18+
private get arch(): string {
19+
switch (this.envSvc.arch) {
20+
case 'arm64':
21+
return 'aarch64';
22+
case 'amd64':
23+
return 'x86_64';
24+
}
25+
}
26+
27+
override async install(version: string): Promise<void> {
28+
const baseUrl = `https://downloads.haskell.org/~ghc/${version}/`;
29+
// use static deb10 binary as it is compatible with all supported ubuntu versions
30+
const filename = `ghc-${version}-${this.arch}-deb10-linux.tar.xz`;
31+
32+
const checksumFile = await this.http.download({
33+
url: `${baseUrl}SHA256SUMS`,
34+
});
35+
const expectedChecksum = (await fs.readFile(checksumFile, 'utf-8'))
36+
.split('\n')
37+
.find((l) => l.includes(filename))
38+
?.split(' ')[0];
39+
40+
const file = await this.http.download({
41+
url: `${baseUrl}${filename}`,
42+
checksumType: 'sha256',
43+
expectedChecksum,
44+
});
45+
46+
await this.pathSvc.ensureToolPath(this.name);
47+
48+
const path = await this.pathSvc.createVersionedToolPath(this.name, version);
49+
await this.compress.extract({
50+
file,
51+
cwd: path,
52+
strip: 1,
53+
});
54+
}
55+
56+
override async link(version: string): Promise<void> {
57+
const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin');
58+
59+
await this.shellwrapper({ srcDir: src });
60+
await this.shellwrapper({ srcDir: src, name: 'ghc-pkg' });
61+
}
62+
63+
override async test(_version: string): Promise<void> {
64+
await this._spawn('ghc', ['--version']);
65+
}
66+
}

src/cli/utils/versions.spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { describe, expect, test } from 'vitest';
2-
import { isValid, parse } from './versions';
2+
import { isFourPartVersion, isValid, parse } from './versions';
33

44
describe('cli/utils/versions', () => {
55
test('isValid', () => {
66
expect(isValid('1.0.0')).toBe(true);
77
expect(isValid('abc')).toBe(false);
88
});
99

10+
test('isFourPartVersion', () => {
11+
expect(isFourPartVersion('1.0.0.0')).toBe(true);
12+
expect(isFourPartVersion('1.0.0')).toBe(false);
13+
expect(isFourPartVersion('abc')).toBe(false);
14+
});
15+
1016
test('parse', () => {
1117
expect(parse('1.0.0')).not.toBeNull();
1218
expect(() => parse('abc')).toThrow('Invalid version: abc');

src/cli/utils/versions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ export function isValid(version: string): boolean {
1212
return semverValid(version) !== null;
1313
}
1414

15+
export function isFourPartVersion(version: string): boolean {
16+
return /^\d+\.\d+\.\d+\.\d+$/.test(version);
17+
}
18+
1519
export function parse(version: string | undefined): SemVer {
1620
const res = semverParse(version);
1721
if (!res) {

test/Dockerfile.distro

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,16 @@ RUN install-tool erlang 28.3.2.0
154154
# renovate: datasource=github-releases packageName=elixir-lang/elixir
155155
RUN install-tool elixir 1.19.5
156156

157+
#--------------------------------------
158+
# Image: test-haskell
159+
#--------------------------------------
160+
FROM build AS test-haskell
161+
162+
# renovate: datasource=github-releases packageName=haskell/cabal versioning=docker
163+
RUN install-tool cabal 3.16.1.0
164+
# renovate: datasource=docker packageName=haskell
165+
RUN install-tool ghc 9.14.1
166+
157167
#--------------------------------------
158168
# Image: test-java
159169
#--------------------------------------
@@ -242,6 +252,7 @@ FROM base
242252
COPY --from=test /.dummy /.dummy
243253
COPY --from=test-docker /.dummy /.dummy
244254
COPY --from=test-erlang /.dummy /.dummy
255+
COPY --from=test-haskell /.dummy /.dummy
245256
COPY --from=test-java /.dummy /.dummy
246257
COPY --from=test-mono /.dummy /.dummy
247258
COPY --from=test-node /.dummy /.dummy

0 commit comments

Comments
 (0)