Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"tofu",
"uv",
"vendir",
"vp",
"wally",
"yarn"
],
Expand Down Expand Up @@ -160,6 +161,7 @@
"tofu",
"uv",
"vendir",
"vp",
"wally",
"yarn"
],
Expand Down
14 changes: 14 additions & 0 deletions docs/custom-registries.md
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,20 @@ Samples:
https://github.com/vmware-tanzu/carvel-vendir/releases/download/v0.22.0/vendir-linux-amd64
```

## `vp`

Vite+ releases are downloaded from:

- `https://github.com/voidzero-dev/vite-plus/releases`

Release archives and their checksum manifest follow these paths:

```txt
https://github.com/voidzero-dev/vite-plus/releases/download/v<version>/vp-x86_64-unknown-linux-gnu.tar.gz
https://github.com/voidzero-dev/vite-plus/releases/download/v<version>/vp-aarch64-unknown-linux-gnu.tar.gz
https://github.com/voidzero-dev/vite-plus/releases/download/v<version>/vp-checksums.txt
```

## `wally`

Wally releases are downloaded from:
Expand Down
2 changes: 2 additions & 0 deletions src/cli/install-tool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ import { SwiftInstallService } from '../tools/swift.ts';
import { TerraformInstallService } from '../tools/terraform.ts';
import { TofuInstallService } from '../tools/tofu.ts';
import { VendirInstallService } from '../tools/vendir.ts';
import { VpInstallService } from '../tools/vp.ts';
import { WallyInstallService } from '../tools/wally.ts';
import { type InstallToolType, logger } from '../utils/index.ts';
import { isNotKnownV2Tool } from '../utils/v2-tool.ts';
Expand Down Expand Up @@ -192,6 +193,7 @@ async function prepareInstallContainer(): Promise<Container> {
container.bind(INSTALL_TOOL_TOKEN).to(TerraformInstallService);
container.bind(INSTALL_TOOL_TOKEN).to(TofuInstallService);
container.bind(INSTALL_TOOL_TOKEN).to(VendirInstallService);
container.bind(INSTALL_TOOL_TOKEN).to(VpInstallService);
container.bind(INSTALL_TOOL_TOKEN).to(WallyInstallService);
container.bind(INSTALL_TOOL_TOKEN).to(YarnInstallService);
container.bind(INSTALL_TOOL_TOKEN).to(YarnSlimInstallService);
Expand Down
1 change: 1 addition & 0 deletions src/cli/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const NoPrepareTools = [
'tofu',
'uv',
'vendir',
'vp',
'wally',
'yarn',
'yarn-slim',
Expand Down
142 changes: 142 additions & 0 deletions src/cli/tools/vp.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import fs from 'node:fs/promises';
import { arch } from 'node:os';
import { join } from 'node:path';
import type { Container } from 'inversify';
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';

import {
CompressionService,
HttpService,
LinkToolService,
} from '../services/index.ts';
import {
VP_SYNC_VERSIONS_UNAVAILABLE,
VpInstallService,
parseVitePlusChecksum,
vitePlusAssetName,
} from './vp.ts';
import { testContainer } from '~test/di.ts';
import { ensurePaths } from '~test/path.ts';

vi.mock('execa');

describe('cli/tools/vp', () => {
describe('release assets', () => {
test.each([
['amd64', 'vp-x86_64-unknown-linux-gnu.tar.gz'],
['arm64', 'vp-aarch64-unknown-linux-gnu.tar.gz'],
] as const)('maps %s to the official release asset', (arch, expected) => {
expect(vitePlusAssetName(arch)).toBe(expected);
});

test('selects an exact release asset checksum', () => {
expect(
parseVitePlusChecksum(
[
'a'.repeat(64) + ' vp-aarch64-unknown-linux-gnu.tar.gz',
'b'.repeat(64) + ' vp-x86_64-unknown-linux-gnu.tar.gz',
'',
].join('\n'),
'vp-x86_64-unknown-linux-gnu.tar.gz',
),
).toBe('b'.repeat(64));
});

test('rejects missing or malformed checksums', () => {
expect(() =>
parseVitePlusChecksum('', 'vp-x86_64-unknown-linux-gnu.tar.gz'),
).toThrow('Cannot find checksum');
expect(() =>
parseVitePlusChecksum(
'not-a-checksum vp-x86_64-unknown-linux-gnu.tar.gz',
'vp-x86_64-unknown-linux-gnu.tar.gz',
),
).toThrow('Cannot find checksum');
});
});

describe('VpInstallService', () => {
let child: Container;
let service: VpInstallService;

beforeAll(async () => {
await ensurePaths([
'opt/containerbase/bin',
'opt/containerbase/tools',
'tmp/containerbase',
'var/lib/containerbase',
]);
});

beforeEach(async () => {
child = await testContainer();
child.bind(HttpService).toSelf();
child.bind(CompressionService).toSelf();
child.bind(LinkToolService).toSelf();
child.bind(VpInstallService).toSelf();
service = await child.getAsync(VpInstallService);
});

test('downloads, verifies, and extracts the exact prebuilt release', async () => {
const filename = vitePlusAssetName(
arch() === 'arm64' ? 'arm64' : 'amd64',
);
const checksum = 'c'.repeat(64);
const checksumFile = join(globalThis.cacheDir, 'vp-checksums.txt');
const archiveFile = join(globalThis.cacheDir, filename);
await fs.writeFile(checksumFile, `${checksum} ${filename}\n`);
await fs.writeFile(archiveFile, 'archive');

const download = vi
.spyOn(HttpService.prototype, 'download')
.mockResolvedValueOnce(checksumFile)
.mockResolvedValueOnce(archiveFile);
vi.spyOn(HttpService.prototype, 'exists').mockResolvedValueOnce(true);
const extract = vi
.spyOn(CompressionService.prototype, 'extract')
.mockResolvedValueOnce();

await service.install('0.4.0');

expect(download).toHaveBeenNthCalledWith(1, {
url: 'https://github.com/voidzero-dev/vite-plus/releases/download/v0.4.0/vp-checksums.txt',
});
expect(download).toHaveBeenNthCalledWith(2, {
url: `https://github.com/voidzero-dev/vite-plus/releases/download/v0.4.0/${filename}`,
checksumType: 'sha256',
expectedChecksum: checksum,
});
expect(extract).toHaveBeenCalledWith({
file: archiveFile,
cwd: expect.stringMatching(/\/vp\/0\.4\.0\/bin$/),
});
});

test('rejects releases that predate the bundled planner', async () => {
vi.spyOn(HttpService.prototype, 'exists').mockResolvedValueOnce(false);
const download = vi.spyOn(HttpService.prototype, 'download');

await expect(service.install('0.3.0')).rejects.toThrow(
`${VP_SYNC_VERSIONS_UNAVAILABLE}:0.3.0`,
);
expect(download).not.toHaveBeenCalled();
});

test('links vp with the Node runtime needed by the bundled planner', async () => {
const shellwrapper = vi
.spyOn(LinkToolService.prototype, 'shellwrapper')
.mockResolvedValueOnce();

await service.link('0.4.0');

expect(shellwrapper).toHaveBeenCalledWith('vp', {
srcDir: expect.stringMatching(/\/vp\/0\.4\.0\/bin$/),
extraToolEnvs: ['node'],
});
});

test('checks the installed vp version', async () => {
await expect(service.test('0.4.0')).resolves.toBeUndefined();
});
});
});
77 changes: 77 additions & 0 deletions src/cli/tools/vp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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';
import type { Arch } from '../utils/index.ts';

// Stable machine-readable marker consumed by Renovate. Do not reword it.
export const VP_SYNC_VERSIONS_UNAVAILABLE =
'CONTAINERBASE_VP_SYNC_VERSIONS_UNAVAILABLE';

export function vitePlusAssetName(arch: Arch): string {
const target = arch === 'arm64' ? 'aarch64' : 'x86_64';
return `vp-${target}-unknown-linux-gnu.tar.gz`;
}

export function parseVitePlusChecksum(
checksums: string,
filename: string,
): string {
for (const line of checksums.split('\n')) {
const match = /^([a-f\d]{64})\s+\*?(.+)$/i.exec(line.trim());
const checksum = match?.[1];
if (checksum && match?.[2] === filename) {
return checksum.toLowerCase();
}
}
throw new Error(`Cannot find checksum for '${filename}' in vp-checksums.txt`);
}

@injectable()
@injectFromHierarchy()
export class VpInstallService extends BaseInstallService {
readonly name = 'vp';
override readonly parent = 'node';

override async install(version: string): Promise<void> {
const baseUrl = `https://github.com/voidzero-dev/vite-plus/releases/download/v${version}/`;
const filename = vitePlusAssetName(this.envSvc.arch);
const checksumUrl = `${baseUrl}vp-checksums.txt`;

if (!(await this.http.exists(checksumUrl))) {
throw new Error(
`${VP_SYNC_VERSIONS_UNAVAILABLE}:${version}: Vite+ release does not provide the sync-versions planner`,
);
}

const checksumFile = await this.http.download({
url: checksumUrl,
});
const expectedChecksum = parseVitePlusChecksum(
await fs.readFile(checksumFile, 'utf8'),
filename,
);
const file = await this.http.download({
url: `${baseUrl}${filename}`,
checksumType: 'sha256',
expectedChecksum,
});

await this.pathSvc.ensureToolPath(this.name);
const path = join(
await this.pathSvc.createVersionedToolPath(this.name, version),
'bin',
);
await fs.mkdir(path);
await this.compress.extract({ file, cwd: path });
}

override async link(version: string): Promise<void> {
const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin');
await this.shellwrapper({ srcDir: src, extraToolEnvs: ['node'] });
}

override async test(_version: string): Promise<void> {
await this._spawn(this.name, ['--version']);
}
}
Loading