Skip to content

Commit f8c606e

Browse files
authored
fix(updater): support zstd manifest entries (#3669)
1 parent 0b67511 commit f8c606e

5 files changed

Lines changed: 114 additions & 18 deletions

File tree

‎.changeset/native-zstd-manifest.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": patch
3+
---
4+
5+
Support compressed downloads from updated native release manifests.

‎apps/kimi-code/src/cli/update/native-manifest.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
* staged updater reuses the same file so checksums and file names have a
66
* single source of truth. Entries point at the bare platform binary
77
* (`kimi-code-<target>[.exe]`), not an archive; an entry may additionally
8-
* carry `compressed`, pointing at the zstd-compressed variant of that binary.
8+
* carry `zstd` (or the legacy `compressed` field), pointing at the
9+
* zstd-compressed variant of that binary.
910
*/
1011

1112
import { valid } from 'semver';
@@ -18,6 +19,12 @@ const MANIFEST_FETCH_TIMEOUT_MS = 10_000;
1819
const PlatformEntrySchema = z.object({
1920
filename: z.string().min(1),
2021
checksum: z.string().regex(/^[a-f0-9]{64}$/, { error: 'invalid sha256' }),
22+
zstd: z
23+
.object({
24+
file: z.string().min(1),
25+
sha256: z.string().regex(/^[a-f0-9]{64}$/, { error: 'invalid sha256' }),
26+
})
27+
.optional(),
2128
compressed: z
2229
.object({
2330
filename: z.string().min(1),

‎apps/kimi-code/src/cli/update/native-stage.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -488,17 +488,20 @@ export async function stageNativeUpdate(
488488
try {
489489
const manifest = await fetchNativeReleaseManifest(options.version, fetchImpl);
490490
const entry = selectPlatformEntry(manifest, platform, arch);
491+
const compressed = entry.zstd === undefined
492+
? entry.compressed
493+
: { filename: entry.zstd.file, checksum: entry.zstd.sha256 };
491494
// Prefer the zstd-compressed artifact when the manifest carries one and
492495
// the runtime can inflate it (~4x smaller than the bare binary). Any
493496
// failure in the compressed path falls back to the bare download below.
494497
let size: number | undefined;
495-
if (entry.compressed !== undefined && typeof createZstdDecompress === 'function') {
498+
if (compressed !== undefined && typeof createZstdDecompress === 'function') {
496499
const zstPartPath = join(stagingDir, `${exeFileName}.zst.part`);
497500
try {
498501
await downloadAndHash(
499-
nativeBinaryUrl(options.version, entry.compressed.filename),
502+
nativeBinaryUrl(options.version, compressed.filename),
500503
zstPartPath,
501-
entry.compressed.checksum,
504+
compressed.checksum,
502505
fetchImpl,
503506
options.onProgress,
504507
options.idleTimeoutMs,

‎apps/kimi-code/test/cli/update/native-manifest.test.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,40 @@ describe('fetchNativeReleaseManifest', () => {
7777
});
7878
});
7979

80+
it('parses a platform entry carrying a zstd artifact pointer', async () => {
81+
const body = JSON.stringify({
82+
version: VERSION,
83+
platforms: {
84+
'linux-x64': {
85+
filename: 'kimi-code-linux-x64',
86+
checksum: 'a'.repeat(64),
87+
zstd: { file: 'kimi-code-linux-x64.zst', sha256: 'b'.repeat(64) },
88+
},
89+
},
90+
});
91+
const manifest = await fetchNativeReleaseManifest(VERSION, mockFetch({ ok: true, status: 200, body }));
92+
expect(manifest.platforms['linux-x64']).toEqual({
93+
filename: 'kimi-code-linux-x64',
94+
checksum: 'a'.repeat(64),
95+
zstd: { file: 'kimi-code-linux-x64.zst', sha256: 'b'.repeat(64) },
96+
});
97+
});
98+
99+
it.each([
100+
{ file: '', sha256: 'b'.repeat(64) },
101+
{ file: 'kimi-code-linux-x64.zst', sha256: 'invalid' },
102+
])('rejects an invalid zstd artifact pointer: %j', async (zstd) => {
103+
const body = JSON.stringify({
104+
version: VERSION,
105+
platforms: {
106+
'linux-x64': { filename: 'kimi-code-linux-x64', checksum: 'a'.repeat(64), zstd },
107+
},
108+
});
109+
await expect(
110+
fetchNativeReleaseManifest(VERSION, mockFetch({ ok: true, status: 200, body })),
111+
).rejects.toThrow();
112+
});
113+
80114
it('rejects a non-semver version argument before hitting the network', async () => {
81115
const f = mockFetch({ ok: true, status: 200, body: MANIFEST_BODY });
82116
await expect(fetchNativeReleaseManifest('nope', f)).rejects.toThrow(/invalid semver/);

‎apps/kimi-code/test/cli/update/native-stage.test.ts‎

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ interface MockCdnOptions {
9696
* When set, the manifest entry advertises a compressed artifact; the .zst
9797
* URL serves `payload` (null → 404, a CDN without the artifact yet).
9898
*/
99-
readonly compressed?: { readonly payload: Buffer | null; readonly checksum?: string };
99+
readonly compressed?: {
100+
readonly payload: Buffer | null;
101+
readonly checksum?: string;
102+
readonly field?: 'compressed' | 'zstd';
103+
};
100104
}
101105

102106
function mockCdnFetch(options: MockCdnOptions): typeof fetch {
@@ -106,11 +110,13 @@ function mockCdnFetch(options: MockCdnOptions): typeof fetch {
106110
checksum: options.checksum ?? sha256Hex(options.payload),
107111
};
108112
if (options.compressed !== undefined) {
109-
platformEntry['compressed'] = {
110-
filename: COMPRESSED_FILENAME,
111-
checksum:
112-
options.compressed.checksum ?? sha256Hex(options.compressed.payload ?? Buffer.alloc(0)),
113-
};
113+
const checksum =
114+
options.compressed.checksum ?? sha256Hex(options.compressed.payload ?? Buffer.alloc(0));
115+
if (options.compressed.field === 'zstd') {
116+
platformEntry['zstd'] = { file: COMPRESSED_FILENAME, sha256: checksum };
117+
} else {
118+
platformEntry['compressed'] = { filename: COMPRESSED_FILENAME, checksum };
119+
}
114120
}
115121
const manifestBody = JSON.stringify({
116122
version,
@@ -201,10 +207,10 @@ describe('stageNativeUpdate', () => {
201207
expect(leftovers).toEqual([]);
202208
});
203209

204-
it('downloads the compressed artifact and stages the decompressed binary', async () => {
210+
it.each(['compressed', 'zstd'] as const)('downloads the %s artifact and stages the decompressed binary', async (field) => {
205211
const fetchImpl = mockCdnFetch({
206212
payload: PAYLOAD,
207-
compressed: { payload: zstdCompressSync(PAYLOAD) },
213+
compressed: { payload: zstdCompressSync(PAYLOAD), field },
208214
});
209215
const result = await stageNativeUpdate({
210216
version: VERSION,
@@ -232,11 +238,11 @@ describe('stageNativeUpdate', () => {
232238
expect(leftovers).toEqual([]);
233239
});
234240

235-
it('falls back to the bare binary when the compressed artifact is missing', async () => {
241+
it.each(['compressed', 'zstd'] as const)('falls back to the bare binary when the compressed artifact is missing (%s)', async (field) => {
236242
vi.spyOn(console, 'warn').mockImplementation(() => {});
237243
const fetchImpl = mockCdnFetch({
238244
payload: PAYLOAD,
239-
compressed: { payload: null },
245+
compressed: { payload: null, field },
240246
});
241247
const result = await stageNativeUpdate({
242248
version: VERSION,
@@ -255,12 +261,12 @@ describe('stageNativeUpdate', () => {
255261
expect(exeBytes.equals(PAYLOAD)).toBe(true);
256262
});
257263

258-
it('falls back to the bare binary when the compressed artifact fails verification', async () => {
264+
it.each(['compressed', 'zstd'] as const)('falls back to the bare binary when the compressed artifact fails verification (%s)', async (field) => {
259265
vi.spyOn(console, 'warn').mockImplementation(() => {});
260266
const fetchImpl = mockCdnFetch({
261267
payload: PAYLOAD,
262268
// The .zst bytes do not hash to the advertised compressed checksum.
263-
compressed: { payload: zstdCompressSync(PAYLOAD), checksum: 'f'.repeat(64) },
269+
compressed: { payload: zstdCompressSync(PAYLOAD), checksum: 'f'.repeat(64), field },
264270
});
265271
const result = await stageNativeUpdate({
266272
version: VERSION,
@@ -279,13 +285,13 @@ describe('stageNativeUpdate', () => {
279285
expect(exeBytes.equals(PAYLOAD)).toBe(true);
280286
});
281287

282-
it('falls back to the bare binary when the decompressed content fails verification', async () => {
288+
it.each(['compressed', 'zstd'] as const)('falls back to the bare binary when the decompressed content fails verification (%s)', async (field) => {
283289
vi.spyOn(console, 'warn').mockImplementation(() => {});
284290
// The .zst verifies against its own checksum but inflates to something
285291
// other than the bare binary the manifest checksum covers.
286292
const fetchImpl = mockCdnFetch({
287293
payload: PAYLOAD,
288-
compressed: { payload: zstdCompressSync(Buffer.from('other-content')) },
294+
compressed: { payload: zstdCompressSync(Buffer.from('other-content')), field },
289295
});
290296
const result = await stageNativeUpdate({
291297
version: VERSION,
@@ -301,6 +307,47 @@ describe('stageNativeUpdate', () => {
301307
expect(exeBytes.equals(PAYLOAD)).toBe(true);
302308
});
303309

310+
it('prefers zstd when both manifest formats are present', async () => {
311+
const compressedPayload = zstdCompressSync(PAYLOAD);
312+
const fetchImpl = mockCdnFetch({
313+
payload: PAYLOAD,
314+
compressed: { payload: compressedPayload, field: 'zstd' },
315+
});
316+
vi.mocked(fetchImpl).mockResolvedValueOnce(new Response(JSON.stringify({
317+
version: VERSION,
318+
platforms: {
319+
'linux-x64': {
320+
filename: BINARY_FILENAME,
321+
checksum: sha256Hex(PAYLOAD),
322+
zstd: { file: COMPRESSED_FILENAME, sha256: sha256Hex(compressedPayload) },
323+
compressed: { filename: 'legacy.zst', checksum: 'a'.repeat(64) },
324+
},
325+
},
326+
})));
327+
328+
const result = await stageNativeUpdate({
329+
version: VERSION,
330+
exePath,
331+
platform: 'linux',
332+
arch: 'x64',
333+
fetchImpl,
334+
});
335+
336+
expect(await readFile(stagedExePath(exePath, result.staged))).toEqual(PAYLOAD);
337+
expect(fetchImpl).toHaveBeenCalledWith(
338+
nativeBinaryUrl(VERSION, COMPRESSED_FILENAME),
339+
expect.anything(),
340+
);
341+
expect(fetchImpl).not.toHaveBeenCalledWith(
342+
nativeBinaryUrl(VERSION, 'legacy.zst'),
343+
expect.anything(),
344+
);
345+
expect(fetchImpl).not.toHaveBeenCalledWith(
346+
nativeBinaryUrl(VERSION, BINARY_FILENAME),
347+
expect.anything(),
348+
);
349+
});
350+
304351
it('marks the staged exe executable', async () => {
305352
const result = await stageNativeUpdate({
306353
version: VERSION,

0 commit comments

Comments
 (0)