Files
renovate/lib/modules/datasource/apk/parser.ts
Punsoong Khut 795d083264 feat(datasource): add APK datasource support (#40250)
* feat(datasource): add APK datasource support

* remove missing manager link for now

* lint docs

* update tests

* replace binary fixtures

* fix coverage

* Update lib/modules/datasource/apk/readme.md

Co-authored-by: Jamie Tanna <github@jamietanna.co.uk>

* Apply suggestion from @viceice

Co-authored-by: Michael Kriese <michael.kriese@gmx.de>

* use tar instead of tar-stream

* use codeBlock

* add ignore back

* make method private

* use an async extract instead of list

* add usage example

* stream apkindex download and parse line-by-line

* refactor(datasource): address review comments on APK datasource

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: sync pnpm-lock.yaml after merge

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: remove ignored lines

As they seem to be covered, anyway.

* fixup! Merge remote-tracking branch 'origin/HEAD' into apk-datasource

* fixup! fixup! Merge remote-tracking branch 'origin/HEAD' into apk-datasource

* lint: use `node:` prefix

* Update lib/modules/datasource/apk/index.ts

Co-authored-by: Michael Kriese <michael.kriese@gmx.de>

* refactor(datasource/apk): use asTimestamp millis instead of new Date

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(datasource/apk): rename uncached method to _getPackages

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(datasource/apk): encode registry URL parts as query parameters

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(datasource/apk): reject invalid registry URLs, skip caching empty indexes

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(datasource/apk): throw on unparseable registry URL and drop unset sourceUrl support

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(datasource/apk): show how registry URL parameters build the index URLs

Co-authored-by: Cursor <cursoragent@cursor.com>

* Apply suggestions from code review

Co-authored-by: RahulGautamSingh <rahultesnik@gmail.com>

* fix(datasource/apk): keep credentials and port in component URLs

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(datasource/apk): extract APKINDEX by basename and key packages by name

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(datasource/apk): report a version served by two components only once

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Jamie Tanna <github@jamietanna.co.uk>
Co-authored-by: Michael Kriese <michael.kriese@gmx.de>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Jamie Tanna <jamie.tanna@mend.io>
Co-authored-by: RahulGautamSingh <rahultesnik@gmail.com>
2026-09-04 12:25:21 +00:00

89 lines
2.2 KiB
TypeScript

import readline from 'node:readline';
import is from '@sindresorhus/is';
import { logger } from '../../../logger/index.ts';
import * as fs from '../../../util/fs/index.ts';
import type { ApkPackage } from './types.ts';
function applyApkIndexLine(
line: string,
packageInfo: Partial<ApkPackage>,
): void {
const colonIndex = line.indexOf(':');
if (colonIndex === -1) {
return;
}
// get fields based on the PKGINFO spec - https://wiki.alpinelinux.org/wiki/Apk_spec
const key = line.substring(0, colonIndex);
const value = line.substring(colonIndex + 1).trim();
switch (key) {
case 'P':
packageInfo.name = value;
break;
case 'V':
packageInfo.version = value;
break;
case 'U':
packageInfo.url = value;
break;
case 't':
packageInfo.buildDate = parseInt(value, 10);
break;
default:
break;
}
}
function flushApkPackage(packageInfo: Partial<ApkPackage>): ApkPackage | null {
if (packageInfo.name && packageInfo.version) {
return packageInfo as ApkPackage;
}
if (is.nonEmptyObject(packageInfo)) {
logger.warn(
{ packageInfo },
'Skipping package entry due to missing required fields',
);
}
return null;
}
/**
* Parses an APK index file line-by-line to avoid loading large indexes into memory.
*
* @param extractedFile - Path to the extracted `APKINDEX` file (relative to Renovate cache).
*/
export async function parseApkIndexFile(
extractedFile: string,
): Promise<ApkPackage[]> {
logger.debug(`Parsing APK index file ${extractedFile}`);
const packages: ApkPackage[] = [];
const rl = readline.createInterface({
input: fs.createCacheReadStream(extractedFile),
terminal: false,
});
let packageInfo: Partial<ApkPackage> = {};
for await (const line of rl) {
if (line === '') {
const pkg = flushApkPackage(packageInfo);
if (pkg) {
packages.push(pkg);
}
packageInfo = {};
} else {
applyApkIndexLine(line, packageInfo);
}
}
const last = flushApkPackage(packageInfo);
if (last) {
packages.push(last);
}
logger.debug(`Parsed ${packages.length} packages from APK index`);
return packages;
}