npm-pkgbuild
Version:
create ArchLinux, RPM and Debian packages from npm packages
255 lines (226 loc) • 6.54 kB
JavaScript
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;
}
}