From f737a07371565d48208be3aa69a791b301637020 Mon Sep 17 00:00:00 2001 From: Amin Yahyaabadi Date: Sun, 23 Mar 2025 00:57:08 -0700 Subject: [PATCH] feat: add setup-alpine package --- packages/setup-alpine/.eslintrc.json | 3 + packages/setup-alpine/package.json | 56 +++++++++++++++++++ packages/setup-alpine/src/apk-repository.ts | 39 +++++++++++++ packages/setup-alpine/src/has-apk.ts | 15 +++++ packages/setup-alpine/src/index.ts | 59 ++++++++++++++++++++ packages/setup-alpine/src/qualify-install.ts | 38 +++++++++++++ packages/setup-alpine/src/update.ts | 7 +++ packages/setup-alpine/tsconfig.json | 9 +++ pnpm-lock.yaml | 40 +++++++++++++ 9 files changed, 266 insertions(+) create mode 100644 packages/setup-alpine/.eslintrc.json create mode 100644 packages/setup-alpine/package.json create mode 100644 packages/setup-alpine/src/apk-repository.ts create mode 100644 packages/setup-alpine/src/has-apk.ts create mode 100644 packages/setup-alpine/src/index.ts create mode 100644 packages/setup-alpine/src/qualify-install.ts create mode 100644 packages/setup-alpine/src/update.ts create mode 100644 packages/setup-alpine/tsconfig.json diff --git a/packages/setup-alpine/.eslintrc.json b/packages/setup-alpine/.eslintrc.json new file mode 100644 index 00000000..be97c53f --- /dev/null +++ b/packages/setup-alpine/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.json" +} diff --git a/packages/setup-alpine/package.json b/packages/setup-alpine/package.json new file mode 100644 index 00000000..6ef3b14a --- /dev/null +++ b/packages/setup-alpine/package.json @@ -0,0 +1,56 @@ +{ + "name": "setup-alpine", + "version": "1.0.0", + "description": "Setup apk packages and repositories in Alpine Linux distributions", + "repository": "https://github.com/aminya/setup-cpp", + "homepage": "https://github.com/aminya/setup-cpp/tree/master/packages/setup-alpine", + "license": "Apache-2.0", + "author": "Amin Yahyaabadi", + "main": "./dist/index.js", + "source": "./src/index.ts", + "type": "module", + "files": [ + "dist", + "src", + "tsconfig.json" + ], + "scripts": { + "build": "tsc --pretty", + "dev": "tsc --watch --pretty", + "lint.tsc": "tsc --noEmit --pretty", + "lint.eslint": "eslint '**/*.{ts,tsx,js,jsx,cjs,mjs,json,yaml}' --no-error-on-unmatched-pattern --cache --cache-location ./.cache/eslint/ --fix", + "prepublishOnly": "pnpm run build" + }, + "dependencies": { + "@types/node": "22.13.11", + "admina": "^1.0.1", + "ci-info": "^4.0.0", + "path-exists": "^5.0.0", + "ci-log": "workspace:*", + "envosman": "workspace:*", + "which": "4.0.0", + "escape-string-regexp": "^5.0.0", + "node-downloader-helper": "2.1.9", + "memoizee": "^0.4.17" + }, + "engines": { + "node": ">=12" + }, + "keywords": [ + "setup", + "apk", + "apk-add", + "repository", + "alpine", + "install", + "setup-apk", + "repositories", + "linux", + "alpine-linux", + "package" + ], + "devDependencies": { + "@types/memoizee": "0.4.11", + "@types/which": "~3.0.4" + } +} diff --git a/packages/setup-alpine/src/apk-repository.ts b/packages/setup-alpine/src/apk-repository.ts new file mode 100644 index 00000000..223e12c2 --- /dev/null +++ b/packages/setup-alpine/src/apk-repository.ts @@ -0,0 +1,39 @@ +import { execRoot } from "admina" +import { info, warning } from "ci-log" +import { pathExists } from "path-exists" +import { hasApk } from "./has-apk.js" + +/** + * Add an APK repository + * @param repoUrl The URL of the repository to add + * @returns Whether the repository was added successfully + */ + +export async function addApkRepository(repoUrl: string): Promise { + if (!(await hasApk())) { + warning("apk is not available on this system") + return false + } + + try { + // Check if repositories file exists + const reposFile = "/etc/apk/repositories" + if (!(await pathExists(reposFile))) { + warning(`APK repositories file not found at ${reposFile}`) + return false + } + + // Add repository to the file + info(`Adding repository: ${repoUrl}`) + await execRoot("sh", ["-c", `echo "${repoUrl}" >> ${reposFile}`], { stdio: "inherit" }) + + // Update package index after adding repository + await execRoot("apk", ["update"], { stdio: "inherit" }) + + info(`Successfully added repository: ${repoUrl}`) + return true + } catch (error) { + warning(`Failed to add repository ${repoUrl}: ${error}`) + return false + } +} diff --git a/packages/setup-alpine/src/has-apk.ts b/packages/setup-alpine/src/has-apk.ts new file mode 100644 index 00000000..1dce9ec2 --- /dev/null +++ b/packages/setup-alpine/src/has-apk.ts @@ -0,0 +1,15 @@ +import memoizee from "memoizee" +import which from "which" + +async function hasApk_() { + try { + await which("apk") + return true + } catch (error) { + return false + } +} +/** + * Check if apk is available on the system + */ +export const hasApk = memoizee(hasApk_, { promise: true }) diff --git a/packages/setup-alpine/src/index.ts b/packages/setup-alpine/src/index.ts new file mode 100644 index 00000000..b2e2421f --- /dev/null +++ b/packages/setup-alpine/src/index.ts @@ -0,0 +1,59 @@ +import { execRoot } from "admina" +import { info, warning } from "ci-log" +import { hasApk } from "./has-apk.js" +import { type ApkPackage, filterAndQualifyApkPackages } from "./qualify-install.js" +import { updateApkMemoized } from "./update.js" + +export type { ApkPackage } + +/** + * Options for installing APK packages + */ +export interface InstallApkPackageOptions { + /** The package name to install */ + package: ApkPackage + /** Whether to update the package index before installing */ + update?: boolean +} + +/** + * Install a package using Alpine's apk package manager + * @param packages The packages to install + * @param update Whether to update the package index before installing + * @returns Whether the installation was successful + */ +export async function installApkPackage(packages: ApkPackage[], update = false): Promise { + // Check if apk is available + if (!(await hasApk())) { + warning("apk is not available on this system") + return false + } + + try { + // Update package index if requested + + if (update) { + // Force update the repos + await updateApkMemoized.clear() + } + // Update the repos if needed + await updateApkMemoized() + + const packagesToInstall = await filterAndQualifyApkPackages(packages) + + if (packagesToInstall.length === 0) { + info("All packages are already installed") + return true + } + + // Install the packages + info(`Installing ${packagesToInstall.map((pack) => pack.name).join(" ")}`) + await execRoot("apk", ["add", ...packagesToInstall.map((pack) => pack.name)], { stdio: "inherit" }) + + info(`Successfully installed ${packagesToInstall.map((pack) => pack.name).join(" ")}`) + return true + } catch (error) { + warning(`Failed to install ${packages.map((pack) => pack.name).join(" ")}: ${error}`) + return false + } +} diff --git a/packages/setup-alpine/src/qualify-install.ts b/packages/setup-alpine/src/qualify-install.ts new file mode 100644 index 00000000..7e06f1be --- /dev/null +++ b/packages/setup-alpine/src/qualify-install.ts @@ -0,0 +1,38 @@ +import { execRoot } from "admina" + +/** + * The information about an apt package + */ +export type ApkPackage = { + /** The name of the package */ + name: string + /** The version of the package (optional) */ + version?: string + /** The repository to add before installing the package (optional) */ + repository?: string + /** + * If the given version is not available, fall back to the latest version + * @default false + */ + fallBackToLatest?: boolean +} + +export async function filterAndQualifyApkPackages(packages: ApkPackage[]) { + // Filter out packages that are already installed + const installedPackages = await Promise.all(packages.map(checkPackageInstalled)) + return packages.filter((_pack, index) => !installedPackages[index]) +} +/** + * Check if a package is already installed + * @param pack The package to check + * @returns Whether the package is installed + */ + +async function checkPackageInstalled(pack: ApkPackage): Promise { + try { + const { exitCode } = await execRoot("apk", ["info", "-e", pack.name], { reject: false }) + return exitCode === 0 + } catch (error) { + return false + } +} diff --git a/packages/setup-alpine/src/update.ts b/packages/setup-alpine/src/update.ts new file mode 100644 index 00000000..8247e058 --- /dev/null +++ b/packages/setup-alpine/src/update.ts @@ -0,0 +1,7 @@ +import { execRoot } from "admina" +import memoizee from "memoizee" + +async function updateApk() { + await execRoot("apk", ["update"], { stdio: "inherit" }) +} +export const updateApkMemoized = memoizee(updateApk, { promise: true }) diff --git a/packages/setup-alpine/tsconfig.json b/packages/setup-alpine/tsconfig.json new file mode 100644 index 00000000..e91599e6 --- /dev/null +++ b/packages/setup-alpine/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "noEmit": false, + "allowImportingTsExtensions": false + }, + "include": ["./src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c63218e..688f8050 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -364,6 +364,46 @@ importers: specifier: ^3.0.0 version: 3.0.4 + packages/setup-alpine: + dependencies: + '@types/node': + specifier: 22.13.11 + version: 22.13.11 + admina: + specifier: ^1.0.1 + version: 1.0.1 + ci-info: + specifier: ^4.0.0 + version: 4.2.0 + ci-log: + specifier: workspace:* + version: link:../ci-log + envosman: + specifier: workspace:* + version: link:../envosman + escape-string-regexp: + specifier: ^5.0.0 + version: 5.0.0 + memoizee: + specifier: ^0.4.17 + version: 0.4.17 + node-downloader-helper: + specifier: 2.1.9 + version: 2.1.9 + path-exists: + specifier: ^5.0.0 + version: 5.0.0 + which: + specifier: 4.0.0 + version: 4.0.0 + devDependencies: + '@types/memoizee': + specifier: 0.4.11 + version: 0.4.11 + '@types/which': + specifier: ~3.0.4 + version: 3.0.4 + packages/setup-apt: dependencies: '@types/node':