From fc2db95d7d4240603c030ddf869a4c72540ce836 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:20:11 -0700 Subject: [PATCH 1/8] feat(tool): install nub from GitHub release binaries --- src/cli/install-tool/index.ts | 2 ++ src/cli/tools/index.ts | 1 + src/cli/tools/nub.ts | 53 +++++++++++++++++++++++++++++++++++ test/latest/Dockerfile | 3 ++ test/latest/Dockerfile.arm64 | 9 ++++++ 5 files changed, 68 insertions(+) create mode 100644 src/cli/tools/nub.ts diff --git a/src/cli/install-tool/index.ts b/src/cli/install-tool/index.ts index 204f5b4448..49da1a51a3 100644 --- a/src/cli/install-tool/index.ts +++ b/src/cli/install-tool/index.ts @@ -81,6 +81,7 @@ import { YarnVersionResolver, } from '../tools/node/resolver.ts'; import { NpmBaseInstallService } from '../tools/node/utils.ts'; +import { NubInstallService } from '../tools/nub.ts'; import { ComposerInstallService, ComposerVersionResolver, @@ -182,6 +183,7 @@ async function prepareInstallContainer(): Promise { container.bind(INSTALL_TOOL_TOKEN).to(NixInstallService); container.bind(INSTALL_TOOL_TOKEN).to(NugetInstallService); container.bind(INSTALL_TOOL_TOKEN).to(NodeInstallService); + container.bind(INSTALL_TOOL_TOKEN).to(NubInstallService); container.bind(INSTALL_TOOL_TOKEN).to(PaketInstallService); container.bind(INSTALL_TOOL_TOKEN).to(PhpInstallService); container.bind(INSTALL_TOOL_TOKEN).to(PixiInstallService); diff --git a/src/cli/tools/index.ts b/src/cli/tools/index.ts index 439c7003ff..2c1ec6a5e4 100644 --- a/src/cli/tools/index.ts +++ b/src/cli/tools/index.ts @@ -33,6 +33,7 @@ export const NoPrepareTools = [ 'maven', 'mise', 'nix', + 'nub', 'nuget', 'npm', 'paket', diff --git a/src/cli/tools/nub.ts b/src/cli/tools/nub.ts new file mode 100644 index 0000000000..e8885b1394 --- /dev/null +++ b/src/cli/tools/nub.ts @@ -0,0 +1,53 @@ +import fs from 'node:fs/promises'; +import { join } from 'node:path'; +import { injectFromHierarchy, injectable } from 'inversify'; +import { BaseInstallService } from '../install-tool/base-install.service.ts'; + +@injectable() +@injectFromHierarchy() +export class NubInstallService extends BaseInstallService { + readonly name = 'nub'; + + private get ghArch(): string { + switch (this.envSvc.arch) { + case 'arm64': + return 'arm64'; + case 'amd64': + return 'x64'; + } + } + + override async install(version: string): Promise { + const baseUrl = `https://github.com/nubjs/nub/releases/download/v${version}/`; + const filename = `nub-linux-${this.ghArch}.tar.gz`; + const url = `${baseUrl}${filename}`; + + const checksumFile = await this.http.download({ url: `${url}.sha256` }); + const expectedChecksum = (await fs.readFile(checksumFile, 'utf-8')) + .trim() + .split(/\s+/)[0]; + if (!expectedChecksum || !/^[a-f0-9]{64}$/i.test(expectedChecksum)) { + throw new Error(`Invalid checksum for ${filename}`); + } + + const file = await this.http.download({ + url, + checksumType: 'sha256', + expectedChecksum, + }); + + await this.pathSvc.ensureToolPath(this.name); + const path = await this.pathSvc.createVersionedToolPath(this.name, version); + // Preserve the release layout: bin/ and runtime/ are siblings. + await this.compress.extract({ file, cwd: path }); + } + + override async link(version: string): Promise { + const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin'); + await this.shellwrapper({ srcDir: src }); + } + + override async test(_version: string): Promise { + await this._spawn(this.name, ['--version']); + } +} diff --git a/test/latest/Dockerfile b/test/latest/Dockerfile index c930adcef9..8096b1a891 100644 --- a/test/latest/Dockerfile +++ b/test/latest/Dockerfile @@ -225,6 +225,9 @@ RUN install-tool buf v1.73.0 # renovate: datasource=npm RUN install-tool bun 1.4.2 +# renovate: datasource=github-releases packageName=nubjs/nub +RUN install-tool nub 0.9.2 + # renovate: datasource=github-releases packageName=denoland/deno RUN install-tool deno 2.9.7 diff --git a/test/latest/Dockerfile.arm64 b/test/latest/Dockerfile.arm64 index 701a95b277..c02f81a6fe 100644 --- a/test/latest/Dockerfile.arm64 +++ b/test/latest/Dockerfile.arm64 @@ -48,6 +48,14 @@ FROM base AS test-bun # renovate: datasource=npm RUN install-tool bun 1.4.2 +#-------------------------------------- +# Image: nub +#-------------------------------------- +FROM base AS test-nub + +# renovate: datasource=github-releases packageName=nubjs/nub +RUN install-tool nub 0.9.2 + #-------------------------------------- # Image: deno #-------------------------------------- @@ -252,6 +260,7 @@ FROM base COPY --from=test-bazelisk /.dummy /.dummy COPY --from=test-bun /.dummy /.dummy +COPY --from=test-nub /.dummy /.dummy COPY --from=test-deno /.dummy /.dummy COPY --from=test-apko /.dummy /.dummy COPY --from=test-apm /.dummy /.dummy From e27707cb812a051026ac92e2908d79d9d001ec2c Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:34:34 -0700 Subject: [PATCH 2/8] docs: document nub release download URLs --- docs/custom-registries.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/custom-registries.md b/docs/custom-registries.md index ea03a3d890..56b7d006bb 100644 --- a/docs/custom-registries.md +++ b/docs/custom-registries.md @@ -627,6 +627,21 @@ https://github.com/containerbase/node-re2-prebuild/releases/download/1.20.9/linu https://github.com/containerbase/node-re2-prebuild/releases/download/1.20.9/linux-x64-108.br ``` +## `nub` + +Nub releases are downloaded from: + +- `https://github.com/nubjs/nub/releases` + +Samples: + +```txt +https://github.com/nubjs/nub/releases/download/v0.9.2/nub-linux-x64.tar.gz +https://github.com/nubjs/nub/releases/download/v0.9.2/nub-linux-x64.tar.gz.sha256 +https://github.com/nubjs/nub/releases/download/v0.9.2/nub-linux-arm64.tar.gz +https://github.com/nubjs/nub/releases/download/v0.9.2/nub-linux-arm64.tar.gz.sha256 +``` + ## `php` PHP releases are downloaded from: From 63ac1df6683f8b74d6170f22f926c71b4ff613d9 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:04:56 -0700 Subject: [PATCH 3/8] chore(nub): update release examples and tests to v0.9.3 --- docs/custom-registries.md | 8 ++++---- test/latest/Dockerfile | 2 +- test/latest/Dockerfile.arm64 | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/custom-registries.md b/docs/custom-registries.md index 56b7d006bb..1aa4ad877f 100644 --- a/docs/custom-registries.md +++ b/docs/custom-registries.md @@ -636,10 +636,10 @@ Nub releases are downloaded from: Samples: ```txt -https://github.com/nubjs/nub/releases/download/v0.9.2/nub-linux-x64.tar.gz -https://github.com/nubjs/nub/releases/download/v0.9.2/nub-linux-x64.tar.gz.sha256 -https://github.com/nubjs/nub/releases/download/v0.9.2/nub-linux-arm64.tar.gz -https://github.com/nubjs/nub/releases/download/v0.9.2/nub-linux-arm64.tar.gz.sha256 +https://github.com/nubjs/nub/releases/download/v0.9.3/nub-linux-x64.tar.gz +https://github.com/nubjs/nub/releases/download/v0.9.3/nub-linux-x64.tar.gz.sha256 +https://github.com/nubjs/nub/releases/download/v0.9.3/nub-linux-arm64.tar.gz +https://github.com/nubjs/nub/releases/download/v0.9.3/nub-linux-arm64.tar.gz.sha256 ``` ## `php` diff --git a/test/latest/Dockerfile b/test/latest/Dockerfile index 8096b1a891..c1c9196fce 100644 --- a/test/latest/Dockerfile +++ b/test/latest/Dockerfile @@ -226,7 +226,7 @@ RUN install-tool buf v1.73.0 RUN install-tool bun 1.4.2 # renovate: datasource=github-releases packageName=nubjs/nub -RUN install-tool nub 0.9.2 +RUN install-tool nub 0.9.3 # renovate: datasource=github-releases packageName=denoland/deno RUN install-tool deno 2.9.7 diff --git a/test/latest/Dockerfile.arm64 b/test/latest/Dockerfile.arm64 index c02f81a6fe..fb1e747f93 100644 --- a/test/latest/Dockerfile.arm64 +++ b/test/latest/Dockerfile.arm64 @@ -54,7 +54,7 @@ RUN install-tool bun 1.4.2 FROM base AS test-nub # renovate: datasource=github-releases packageName=nubjs/nub -RUN install-tool nub 0.9.2 +RUN install-tool nub 0.9.3 #-------------------------------------- # Image: deno From 5404b0ac1972e40a82144bc587b77a4e1209c6cc Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:14:36 -0700 Subject: [PATCH 4/8] chore(nub): include tool in test metadata --- .github/renovate.json | 2 ++ test/latest/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 0b2362e818..7f1535b31b 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -91,6 +91,7 @@ "nix", "node", "npm", + "nub", "pdm", "pipenv", "pixi", @@ -146,6 +147,7 @@ "nix", "node", "npm", + "nub", "pdm", "pipenv", "pixi", diff --git a/test/latest/Dockerfile b/test/latest/Dockerfile index c1c9196fce..fca00ae2dc 100644 --- a/test/latest/Dockerfile +++ b/test/latest/Dockerfile @@ -212,7 +212,7 @@ RUN prepare-tool all RUN set -ex; [ -d /usr/local/erlang ] && echo "works" || exit 1; #-------------------------------------- -# test: apm, bazelisk, buf, bun, deno, devbox, gh, helmfile, kustomize, skopeo, tofu, vendir +# test: apm, bazelisk, buf, bun, deno, devbox, gh, helmfile, kustomize, nub, skopeo, tofu, vendir #-------------------------------------- FROM base AS teste From 1df2e3e496b153dd5cfad4f6ff99e85eb4dc5c48 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:21:07 -0700 Subject: [PATCH 5/8] test(nub): cover release installation and checksum validation --- src/cli/tools/nub.spec.ts | 91 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/cli/tools/nub.spec.ts diff --git a/src/cli/tools/nub.spec.ts b/src/cli/tools/nub.spec.ts new file mode 100644 index 0000000000..34584c3e8b --- /dev/null +++ b/src/cli/tools/nub.spec.ts @@ -0,0 +1,91 @@ +import { arch } from 'node:os'; +import { join } from 'node:path'; +import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; +import { CompressionService, LinkToolService } from '../services/index.ts'; +import { NubInstallService } from './nub.ts'; +import { scope } from '~test/http-mock.ts'; +import { ensurePaths } from '~test/path.ts'; +import { checksum, toolContext } from '~test/tool.ts'; + +const { execaMock } = vi.hoisted(() => ({ execaMock: vi.fn() })); +vi.mock('execa', () => ({ execa: execaMock })); +vi.mock('node:os', async (importOriginal) => ({ + ...(await importOriginal()), + arch: vi.fn(() => 'x64'), +})); + +const baseUrl = 'https://github.com'; +const archive = 'nub archive'; + +describe('cli/tools/nub', () => { + beforeAll(async () => { + await ensurePaths(['tmp', 'opt/containerbase/bin']); + }); + + beforeEach(() => { + vi.mocked(arch).mockReturnValue('x64'); + execaMock.mockResolvedValue({ failed: false }); + }); + + test.each([ + { hostArch: 'x64', ghArch: 'x64', version: '0.9.3' }, + { hostArch: 'arm64', ghArch: 'arm64', version: '0.9.2' }, + ] as const)('install on $ghArch', async ({ hostArch, ghArch, version }) => { + vi.mocked(arch).mockReturnValue(hostArch); + const { svc, pathSvc } = await toolContext(NubInstallService); + const filename = `nub-linux-${ghArch}.tar.gz`; + const releaseUrl = `/nubjs/nub/releases/download/v${version}`; + scope(baseUrl) + .get(`${releaseUrl}/${filename}.sha256`) + .reply(200, `${checksum(archive)} ${filename}\n`) + .get(`${releaseUrl}/${filename}`) + .reply(200, archive); + const extract = vi.spyOn(CompressionService.prototype, 'extract'); + + await expect(svc.install(version)).resolves.toBeUndefined(); + + expect(extract).toHaveBeenCalledExactlyOnceWith({ + file: expect.stringContaining(filename), + cwd: pathSvc.versionedToolPath('nub', version), + }); + }); + + test.each([ + { version: '0.9.4', body: '' }, + { version: '0.9.5', body: 'invalid checksum' }, + ])('rejects invalid checksum $body', async ({ version, body }) => { + const { svc } = await toolContext(NubInstallService); + scope(baseUrl) + .get( + `/nubjs/nub/releases/download/v${version}/nub-linux-x64.tar.gz.sha256`, + ) + .reply(200, body); + + await expect(svc.install(version)).rejects.toThrow( + 'Invalid checksum for nub-linux-x64.tar.gz', + ); + }); + + test('link', async () => { + const { svc, pathSvc } = await toolContext(NubInstallService); + const spy = vi.spyOn(LinkToolService.prototype, 'shellwrapper'); + + await expect(svc.link('0.9.3')).resolves.toBeUndefined(); + + expect(spy).toHaveBeenCalledExactlyOnceWith('nub', { + srcDir: join(pathSvc.versionedToolPath('nub', '0.9.3'), 'bin'), + }); + }); + + test('runs the tool test', async () => { + const { svc } = await toolContext(NubInstallService); + + await expect(svc.test('0.9.3')).resolves.toBeUndefined(); + + expect(execaMock).toHaveBeenCalledWith( + 'nub', + ['--version'], + expect.any(Object), + ); + }); +}); From 4e26629b5fbdc68b0d773b21f0a091702ae69159 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Thu, 24 Sep 2026 07:34:50 -0700 Subject: [PATCH 6/8] refactor(nub): use shared checksum helper --- src/cli/tools/nub.spec.ts | 16 ---------------- src/cli/tools/nub.ts | 9 +-------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/src/cli/tools/nub.spec.ts b/src/cli/tools/nub.spec.ts index 34584c3e8b..313f3b4826 100644 --- a/src/cli/tools/nub.spec.ts +++ b/src/cli/tools/nub.spec.ts @@ -50,22 +50,6 @@ describe('cli/tools/nub', () => { }); }); - test.each([ - { version: '0.9.4', body: '' }, - { version: '0.9.5', body: 'invalid checksum' }, - ])('rejects invalid checksum $body', async ({ version, body }) => { - const { svc } = await toolContext(NubInstallService); - scope(baseUrl) - .get( - `/nubjs/nub/releases/download/v${version}/nub-linux-x64.tar.gz.sha256`, - ) - .reply(200, body); - - await expect(svc.install(version)).rejects.toThrow( - 'Invalid checksum for nub-linux-x64.tar.gz', - ); - }); - test('link', async () => { const { svc, pathSvc } = await toolContext(NubInstallService); const spy = vi.spyOn(LinkToolService.prototype, 'shellwrapper'); diff --git a/src/cli/tools/nub.ts b/src/cli/tools/nub.ts index e8885b1394..711552e643 100644 --- a/src/cli/tools/nub.ts +++ b/src/cli/tools/nub.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs/promises'; import { join } from 'node:path'; import { injectFromHierarchy, injectable } from 'inversify'; import { BaseInstallService } from '../install-tool/base-install.service.ts'; @@ -22,13 +21,7 @@ export class NubInstallService extends BaseInstallService { const filename = `nub-linux-${this.ghArch}.tar.gz`; const url = `${baseUrl}${filename}`; - const checksumFile = await this.http.download({ url: `${url}.sha256` }); - const expectedChecksum = (await fs.readFile(checksumFile, 'utf-8')) - .trim() - .split(/\s+/)[0]; - if (!expectedChecksum || !/^[a-f0-9]{64}$/i.test(expectedChecksum)) { - throw new Error(`Invalid checksum for ${filename}`); - } + const expectedChecksum = await this.getChecksum(`${url}.sha256`); const file = await this.http.download({ url, From e63a6aa5afe98c445395ef3a6ab5fa56b7a26a6e Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:24:45 -0700 Subject: [PATCH 7/8] docs(nub): document installer methods and archive layout --- src/cli/tools/nub.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cli/tools/nub.ts b/src/cli/tools/nub.ts index 711552e643..ba819c4a0a 100644 --- a/src/cli/tools/nub.ts +++ b/src/cli/tools/nub.ts @@ -7,6 +7,7 @@ import { BaseInstallService } from '../install-tool/base-install.service.ts'; export class NubInstallService extends BaseInstallService { readonly name = 'nub'; + /** The architecture name used by the nub release assets. */ private get ghArch(): string { switch (this.envSvc.arch) { case 'arm64': @@ -16,6 +17,7 @@ export class NubInstallService extends BaseInstallService { } } + /** Downloads and extracts the nub release, verified against its SHA-256 checksum. */ override async install(version: string): Promise { const baseUrl = `https://github.com/nubjs/nub/releases/download/v${version}/`; const filename = `nub-linux-${this.ghArch}.tar.gz`; @@ -31,15 +33,17 @@ export class NubInstallService extends BaseInstallService { await this.pathSvc.ensureToolPath(this.name); const path = await this.pathSvc.createVersionedToolPath(this.name, version); - // Preserve the release layout: bin/ and runtime/ are siblings. + // The archive has no top-level folder, so extract without stripping a level. await this.compress.extract({ file, cwd: path }); } + /** Links the nub binary into the global bin folder. */ override async link(version: string): Promise { const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin'); await this.shellwrapper({ srcDir: src }); } + /** Checks that nub runs without unpacking its embedded runtime. */ override async test(_version: string): Promise { await this._spawn(this.name, ['--version']); } From 196109f0ac86628c4f0c426b80617e27231c79a3 Mon Sep 17 00:00:00 2001 From: Michael Kriese Date: Thu, 1 Oct 2026 16:01:34 +0200 Subject: [PATCH 8/8] chore: add nub to the supported tool list Regenerated with pnpm tools after merging main. Co-Authored-By: Claude Opus 5.5 --- packages/base/data/tools.json | 1 + packages/base/src/data.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/base/data/tools.json b/packages/base/data/tools.json index 22e0a7f712..876fb5f8af 100644 --- a/packages/base/data/tools.json +++ b/packages/base/data/tools.json @@ -50,6 +50,7 @@ "nix": {}, "node": {}, "npm": { "type": "npm", "parent": "node" }, + "nub": {}, "nuget": { "parent": "mono" }, "paket": { "parent": "dotnet" }, "pdm": { "type": "pip", "parent": "python" }, diff --git a/packages/base/src/data.ts b/packages/base/src/data.ts index c5caed9900..ad04adb9b4 100644 --- a/packages/base/src/data.ts +++ b/packages/base/src/data.ts @@ -55,6 +55,7 @@ export const tools = { nix: {}, node: {}, npm: { type: 'npm', parent: 'node' }, + nub: {}, nuget: { parent: 'mono' }, paket: { parent: 'dotnet' }, pdm: { type: 'pip', parent: 'python' },