UNPKG

npm-pkgbuild

Version:

create ArchLinux, RPM and Debian packages from npm packages

255 lines (226 loc) 6.54 kB
import { join } from "node:path"; import { readFile } from "node:fs/promises"; import { cp } from "node:fs/promises"; import { execa } from "execa"; import { integer_attribute, url_attribute, string_attribute, string_collection_attribute_writable } from "pacc"; import { ContentEntry, IteratorContentEntry } from "content-entry"; import { transform } from "content-entry-transform"; import { keyValueTransformer, colonSeparatedKeyValuePairOptionsDoublingKeys, Uint8ArraysToLines } from "key-value-transformer"; import { Packager, pkgbuild_version_attribute, pkgbuild_description_attribute, pkgbuild_name_attribute } from "./packager.mjs"; import { copyEntries, fieldProvider, utf8StreamOptions, aggregate } from "../util.mjs"; /** * @typedef {import('../publish.mjs').PublishingDetail} PublishingDetail */ function quoteFile(name) { return name.match(/\s/) ? '"' + name + '"' : name; } /** * Produce rpm packages. */ export class RPM extends Packager { static get name() { return "rpm"; } static get description() { return "generate RPM package"; } static get fileNameExtension() { return ".rpm"; } get packageFileName() { const p = this.properties; return `${p.name}-${p.version}-${p.release}.${p.arch}${this.fileNameExtension}`; } /** * @see https://rpm-packaging-guide.github.io */ static attributes = { Name: { ...pkgbuild_name_attribute, name: "Name" }, Summary: { ...pkgbuild_description_attribute, name: "Summary" }, License: { ...string_attribute, name: "License", alias: "license", mandatory: true }, Version: { ...pkgbuild_version_attribute, name: "Version" }, Release: { ...integer_attribute, name: "Release", alias: "release", default: 1, mandatory: true }, Source0: { ...string_attribute, name: "Source0", alias: "source" }, Group: { ...string_attribute, name: "Group", alias: "groups" }, Packager: { ...string_attribute, name: "Packager", alias: "maintainer" }, Vendor: { ...string_attribute, name: "Vendor", alias: "vendor" }, BuildArch: { ...string_attribute, name: "BuildArch", alias: "arch", default: "noarch", mandatory: true }, URL: { ...url_attribute, name: "URL", alias: "homepage" }, Requires: { ...string_collection_attribute_writable, name: "Requires" }, Obsoletes: { ...string_collection_attribute_writable, name: "Obsoletes" }, Conflicts: { ...string_collection_attribute_writable, name: "Conflicts" } }; static get workspaceLayout() { return { named: { staging: "BUILDROOT" }, others: ["RPMS", "SRPMS", "SOURCES", "SPECS"] }; } /** * Check for rpmbuild presence. * @param {Object} options * @param {Object} variant * @param {string} variant.arch * @returns {Promise<boolean>} true when rpmbuild executable is present */ static async prepare(options, variant) { try { await execa("rpmbuild", ["--version"]); if (variant?.arch) { const uname = await execa("uname", ["-m"]); return uname.stdout.match(variant.arch) ? true : false; } return true; } catch {} return false; } dependencyExpression(name, expression) { return `${this.packageName(name)}${expression .replace(/^\s*(\w+)/, (match, p1) => ` = ${p1}`) .replace(/^\s*$/, "") .replace( /^\s*(<|<=|>|>=|=)\s*(\w+)/, (match, p1, p2) => ` ${p1} ${p2}` )}`; } /** * Map install hook named from default (arch) to rpm. */ get hookMapping() { return { pre_install: "pre", post_install: "post", pre_remove: "preun", post_remove: "postun" /* TODO with logic check $1 pre_upgrade: post_upgrade:*/ }; } async create(sources, transformer, publishingDetails, options, expander) { const { properties, tmpdir, staging, destination } = await this.prepare(options); properties.Requires = this.makeDepends(properties.dependencies); if (properties.Packager?.length > 1) { // TODO how to write several Packages ? properties.Packager.length = 1; } const specFileName = `${properties.name}.spec`; const files = []; const self = this; async function* trailingLines() { yield "%define _unpackaged_files_terminate_build 0\n"; yield `%description\n\n`; for await (const hook of self.hookContent()) { yield `%${hook.name}\n`; yield* hook.string.split("\n").map(l => l + "\n"); } yield `%files\n\n`; for (const file of files) { yield quoteFile("/" + file) + "\n"; } for await (const file of copyEntries( transform(aggregate(sources), transformer), staging, expander )) { if (options.verbose) { console.log(file.destination); } yield file.destination + "\n"; } } const fp = fieldProvider(properties, this.attributes); for await (const file of copyEntries( transform(aggregate(sources), [ { match: entry => entry.name === specFileName, transform: async entry => new IteratorContentEntry( entry.name, undefined, keyValueTransformer(Uint8ArraysToLines(await entry.stream), fp, { ...colonSeparatedKeyValuePairOptionsDoublingKeys, trailingLines }) ), createEntryWhenMissing: () => new ContentEntry(specFileName) } ]), staging, expander )) { files.push(file.destination); } if (options.verbose) { console.log( await readFile(join(staging, specFileName), utf8StreamOptions) ); } const packageFile = join(destination, this.packageFileName); if (!options.dry) { const rpmbuild = await execa("rpmbuild", [ "--define", `_topdir ${tmpdir}`, "--buildroot", staging, "-vv", "-bb", join(staging, specFileName) ]); if (options.verbose) { console.log(rpmbuild.stdout); } await cp( join( tmpdir, "RPMS", // TODO handle arrays ? Array.isArray(properties.arch) ? properties.arch[0] : properties.arch, this.packageFileName ), packageFile, { preserveTimestamps: true } ); } return packageFile; } }