Initial package (close RHBZ#2029461)

This commit is contained in:
Benjamin A. Beasley 2021-12-06 14:53:52 -05:00
parent 320f2ef466
commit f460573ec4
6 changed files with 510 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/llhttp-6.0.6-nm-dev.tgz
/llhttp-6.0.6.tar.gz

View File

@ -0,0 +1,31 @@
[any]
[prod]
[dev]
# Just a module wrapper around the code in tslib, which does have a proper
# license in its package.json:
# tslib/modules
modules = "<unknown version>"
# A “dummy” module in the tests for tslib
# tslib/test/validateModuleExportsMatchCommonJS
validateModuleExportsMatchCommonJS = "<unknown version>"
# These are all “dummy” modules in the tests for resolve:
# resolve/test/module_dir/zmodules/bbb
bbb = "<unknown version>"
# resolve/test/resolver/invalid_main
"invalid main" = "<unknown version>"
# resolve/test/resolver/incorrect_main
incorrect_main = "<unknown version>"
# resolve/test/resolver/dot_slash_main
dot_slash_main = "<unknown version>"
# resolve/test/resolver/dot_main
dot_main = "<unknown version>"
# resolve/test/resolver/baz
baz = "<unknown version>"
# resolve/test/resolver/browser_field
browser_field = "<unknown version>"
# resolve/test/resolver/symlinked/package
package = "<unknown version>"

191
check-null-licenses Executable file
View File

@ -0,0 +1,191 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import json
from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
from pathlib import Path
from sys import exit, stderr
import toml
def main():
args = parse_args()
problem = False
if not args.tree.is_dir():
return f"Not a directory: {args.tree}"
for pjpath in args.tree.glob("**/package.json"):
name, version, license = parse(pjpath)
identity = f"{name} {version}"
if version in args.exceptions.get(name, ()):
continue # Do not even check the license
elif license is None:
problem = True
print(
f"Missing license in package.json for {identity}", file=stderr
)
elif isinstance(license, dict):
if isinstance(license.get("type"), str):
continue
print(
(
"Missing type for (deprecated) license object in "
f"package.json for {identity}: {license}"
),
file=stderr,
)
elif isinstance(license, list):
if license and all(
isinstance(entry, dict) and isinstance(entry.get("type"), str)
for entry in license
):
continue
print(
(
"Defective (deprecated) licenses array-of objects in "
f"package.json for {identity}: {license}"
),
file=stderr,
)
elif isinstance(license, str):
continue
else:
print(
(
"Weird type for license in "
f"package.json for {identity}: {license}"
),
file=stderr,
)
problem = True
if problem:
return "At least one missing license was found."
def check_exception(exceptions, name, version):
x = args.exceptions
def parse(package_json_path):
with package_json_path.open("rb") as pjfile:
pj = json.load(pjfile)
try:
license = pj["license"]
except KeyError:
license = pj.get("licenses")
try:
name = pj["name"]
except KeyError:
name = package_json_path.parent.name
version = pj.get("version", "<unknown version>")
return name, version, license
def parse_args():
parser = ArgumentParser(
formatter_class=RawDescriptionHelpFormatter,
description=(
"Search for bundled dependencies without declared licenses"
),
epilog="""
The exceptions file must be a TOML file with zero or more tables. Each tables
keys are package names; the corresponding values values are exact version
number strings, or arrays of version number strings, that have been manually
audited to determine their license status and should therefore be ignored.
Exceptions in a table called “any” are always applied. Otherwise, exceptions
are applied only if a corresponding --with TABLENAME argument is given;
multiple such arguments may be given.
For
example:
[any]
example-foo = "1.0.0"
[prod]
example-bar = [ "2.0.0", "2.0.1",]
[dev]
example-bat = [ "3.7.4",]
would always ignore version 1.0.0 of example-foo. It would ignore example-bar
2.0.1 only when called with “--with prod”.
Comments may (and should) be used to describe the manual audits upon which the
exclusions are based.
Otherwise, any package.json with missing or null license field in the tree is
considered an error, and the program returns with nonzero status.
""",
)
parser.add_argument(
"-x",
"--exceptions",
type=FileType("r"),
help="Manually audited package versions file",
)
parser.add_argument(
"-w",
"--with",
action="append",
default=[],
help="Enable a table in the exceptions file",
)
parser.add_argument(
"tree",
metavar="node_modules_dir",
type=Path,
help="Path to search recursively",
default=".",
)
args = parser.parse_args()
if args.exceptions is None:
args.exceptions = {}
xname = None
else:
with args.exceptions as xfile:
xname = getattr(xfile, "name", "<exceptions>")
args.exceptions = toml.load(args.exceptions)
if not isinstance(args.exceptions, dict):
parser.error(f"Invalid format in {xname}: not an object")
for tablename, table in args.exceptions.items():
if not isinstance(table, dict):
parser.error(
f"Non-table entry in {xname}: {tablename} = {table!r}"
)
overlay = {}
for key, value in table.items():
if isinstance(value, str):
overlay[key] = [value]
elif not isinstance(value, list) or not all(
isinstance(entry, str) for entry in value
):
parser.error(
f"Invalid format in {xname} in [{tablename}]: "
f"{key!r} = {value!r}"
)
table.update(overlay)
x = args.exceptions.get("any", {})
for add in getattr(args, "with"):
try:
x.update(args.exceptions[add])
except KeyError:
if xname is None:
parser.error(
f"No table {add}, as no exceptions file was given"
)
else:
parser.error(f"No table {add} in {xname}")
# Store the merged dictionary
args.exceptions = x
return args
if __name__ == "__main__":
exit(main())

109
llhttp-packaging-bundler Executable file
View File

@ -0,0 +1,109 @@
#!/bin/bash
set -o nounset
set -o errexit
OUTPUT_DIR="$(rpm -E '%{_sourcedir}')"
SPEC_FILE="${PWD}/llhttp.spec"
usage() {
cat 1>&2 <<EOF
Usage: $(basename "$0")
Given llhttp.spec in the working directory, download the source and the prod
and dev dependencies, each in their own tarball.
Also finds licenses for prod dependencies.
All three tarballs and the license list are copied to
${OUTPUT_DIR}.
EOF
exit 1
}
if ! [[ -f /usr/bin/npm ]]
then
cat 1>&2 <<EOF
$(basename "${0}") requires npm to run
Run the following to fix this:
sudo dnf install npm
EOF
exit 2
fi
if [[ $# -gt 0 ]]; then
usage
fi
TMP_DIR="$(mktemp -d -t ci-XXXXXXXXXX)"
trap "cd /; rm -rf '${TMP_DIR}'" INT TERM EXIT
cd "${TMP_DIR}"
echo "Reading ${SPEC_FILE}; downloading source archive" 1>&2
VERSION="$(awk '$1 == "Version:" { print $2; exit }' "${SPEC_FILE}")"
echo "Version is ${VERSION}" 1>&2
echo "Downloading source archive" 1>&2
spectool -g "${SPEC_FILE}"
ARCHIVE="$(
find . -mindepth 1 -maxdepth 1 -type f -name '*.tar.gz' -print -quit
)"
echo "Downloaded $(basename "${ARCHIVE}")" 1>&2
tar -xzf "${ARCHIVE}"
XDIR="$(find . -mindepth 1 -maxdepth 1 -type d -print -quit)"
echo "Extracted to $(basename "${XDIR}")" 1>&2
cd "${XDIR}"
echo "Downloading prod dependencies" 1>&2
# Compared to nodejs-packaging-bundler, we must add --ignore-scripts or npm
# unsuccessfully attempts to build the package.
npm install --no-optional --only=prod --ignore-scripts
echo "Successful prod dependencies download" 1>&2
mv node_modules/ node_modules_prod
echo "LICENSES IN BUNDLE:"
LICENSE_FILE="${TMP_DIR}/llhttp-${VERSION}-bundled-licenses.txt"
find . -name 'package.json' -exec jq '.license | strings' '{}' ';' \
>> "${LICENSE_FILE}"
for what in '.license | objects | .type' '.licenses[] .type'
do
find . -name 'package.json' -exec jq "${what}" '{}' ';' \
>> "${LICENSE_FILE}" 2>/dev/null
done
sort -u -o "${LICENSE_FILE}" "${LICENSE_FILE}"
# Locate any dependencies without a provided license
find . -type f -name 'package.json' -execdir jq \
'if .license==null and .licenses==null then .name else null end' '{}' '+' |
grep -vE '^null$' |
sort -u > "${TMP_DIR}/nolicense.txt"
if [[ -s "${TMP_DIR}/nolicense.txt" ]]
then
echo -e "\e[5m\e[41mSome dependencies do not list a license. Manual verification required!\e[0m"
cat "${TMP_DIR}/nolicense.txt"
echo -e "\e[5m\e[41m======================================================================\e[0m"
fi
echo "Downloading dev dependencies" 1>&2
# Compared to nodejs-packaging-bundler, we must add --ignore-scripts or npm
# unsuccessfully attempts to build the package.
npm install --no-optional --only=dev --ignore-scripts
echo "Successful dev dependencies download" 1>&2
mv node_modules/ node_modules_dev
if [[ -d node_modules_prod ]]
then
tar -czf "../llhttp-${VERSION}-nm-prod.tgz" node_modules_prod
fi
if [[ -d node_modules_dev ]]
then
tar -czf "../llhttp-${VERSION}-nm-dev.tgz" node_modules_dev
fi
cd ..
find . -mindepth 1 -maxdepth 1 -type f \( -name "$(basename "${ARCHIVE}")" \
-o -name "llhttp-${VERSION}*" \) -exec cp -vp '{}' "${OUTPUT_DIR}" ';'

175
llhttp.spec Normal file
View File

@ -0,0 +1,175 @@
# This package is rather exotic. The compiled library is a typical shared
# library with a C API. However, it has only a tiny bit of C source code. Most
# of the library is written in TypeScript, which is transpiled to C, via LLVM
# IR, using llparse (https://github.com/nodejs/llparse)—all of which happens
# within the NodeJS ecosystem.
#
# The package therefore “builds like” a NodeJS package, and to the extent they
# are relevant we apply the NodeJS packaging guidelines. However, the result of
# the build “installs like” a traditional C library package and has no NodeJS
# dependencies, including bundled ones.
#
# Furthermore, the package is registered with npm as “llhttp”, but current
# releases are not published there, so we use the GitHub archive as the
# canonical source and use a custom bundler script based on
# nodejs-packaging-bundler to fetch NodeJS build dependencies.
#
# Overall, we cherry-pick from the standard and NodeJS packaging guidelines as
# each seems to best apply, understanding that this package does not fit well
# into any of the usual patterns or templates.
# Upstream has been asked to provide a proper .so version:
# https://github.com/nodejs/llhttp/issues/140
# …but for now, we must version the shared library downstream.
%global downstream_soversion 0.1
Name: llhttp
Version: 6.0.6
Release: %autorelease
Summary: Port of http_parser to llparse
# License of llhttp is MIT; nothing from the NodeJS dependency bundle is
# installed, so its contents do not contribute to the license of the binary
# RPMs, and we do not need a file llhttp-%%{version}-bundled-licenses.txt.
License: MIT
%global forgeurl https://github.com/nodejs/llhttp
%forgemeta
URL: %{forgeurl}
Source0: %{forgesource}
# Based closely on nodejs-packaging-bundler, except:
#
# - The GitHub source tarball specified in this spec file is used since the
# current version is not typically published on npm
# - No production dependency bundle is generated, since none is needed—and
# therefore, no bundled licenses text file is generated either
Source1: llhttp-packaging-bundler
# Created with llhttp-packaging-bundler (Source1):
Source2: llhttp-%{version}-nm-dev.tgz
# While nothing in the dev bundle is installed, we still choose to audit for
# null licenses at build time and to keep manually-approved exceptions in a
# file.
Source3: check-null-licenses
Source4: audited-null-licenses.toml
# The compiled RPM does not depend on NodeJS at all, but we cannot *build* it
# on architectures without NodeJS.
ExclusiveArch: %{nodejs_arches}
# For generating the C source “release” from TypeScript:
BuildRequires: nodejs-devel
BuildRequires: make
# For compiling the C library
BuildRequires: cmake
BuildRequires: gcc
# For tests
BuildRequires: clang
# For check-null-licenses
BuildRequires: python3-devel
BuildRequires: python3dist(toml)
%description
This project is a port of http_parser to TypeScript. llparse is used to
generate the output C source file, which could be compiled and linked with the
embedder's program (like Node.js).
%package devel
Summary: Development files for llhttp
Requires: llhttp%{?_isa} = %{?epoch:%{epoch}:}%{version}-%{release}
Requires: cmake-filesystem
%description devel
The llhttp-devel package contains libraries and header files for
developing applications that use llhttp.
%prep
%forgeautosetup
# Set up bundled (dev) node modules required to generate the C sources from the
# TypeScript sources.
tar -xzf '%{SOURCE2}'
mkdir -p node_modules
pushd node_modules
ln -s ../node_modules_dev/* .
ln -s ../node_modules_dev/.bin .
popd
# We run ts-node out of node_modules/.bin rather than using npx (which we will
# not have available).
sed -r -i 's@\bnpx[[:blank:]](ts-node)\b@node_modules/.bin/\1@' Makefile
%build
# Generate the C source “release” from TypeScript using the “node_modules_dev”
# bundle.
%make_build release
# Apply downstream .so versioning
cat >> release/CMakeLists.txt <<'EOF'
set_target_properties(llhttp PROPERTIES SOVERSION %{downstream_soversion})
EOF
# Fix multilib install paths. We hoped this change would be sufficient, but it
# seems to fix the install paths of the CMake files only, so we still need to
# move the libraries after they are installed.
sed -r -i 's@\b(DESTINATION[[:blank:]]+)lib($|/)@\1%{_libdir}\2@' \
release/CMakeLists.txt
# To help prove that nothing from the bundled NodeJS dependencies is included
# in the binary packages, remove the “node_modules” symlinks.
rm -rvf node_modules
cd release
%cmake -DBUILD_SHARED_LIBS:BOOL=ON
%cmake_build
%install
cd release
%cmake_install
if [ '%{_prefix}/lib' != '%{_libdir}' ]
then
mv -v %{buildroot}%{_prefix}/lib/libllhttp.so* '%{buildroot}/%{_libdir}'
# Document the expectation that this directory is now empty:
rmdir '%{buildroot}%{_prefix}/lib'
fi
%check
# Symlink the NodeJS bundle again so that we can test with Mocha
mkdir -p node_modules
pushd node_modules
ln -s ../node_modules_dev/* .
ln -s ../node_modules_dev/.bin .
popd
# Verify that no bundled dev dependency has a null license field, unless we
# already audited it by hand. This reduces the chance of accidentally including
# code with license problems in the source RPM.
%{python3} '%{SOURCE3}' --exceptions '%{SOURCE4}' --with dev node_modules_dev
# See scripts.mocha in package.json:
NODE_ENV=test ./node_modules/.bin/mocha \
-r ts-node/register/type-check \
test/*-test.ts
%files
%license release/LICENSE-MIT
%{_libdir}/libllhttp.so.%{downstream_soversion}
%files devel
%doc release/README.md
%{_includedir}/llhttp.h
%{_libdir}/libllhttp.so
%{_libdir}/cmake/llhttp
%changelog
%autochangelog

2
sources Normal file
View File

@ -0,0 +1,2 @@
SHA512 (llhttp-6.0.6-nm-dev.tgz) = ea8905b57f51ad2d870d17dc579ec5fe2175b3bb898f304af4f1e3bd52782488dfb9bb38281f1a826d1745fa608e0200e52239ea5bd525392ad7150461d03448
SHA512 (llhttp-6.0.6.tar.gz) = 6d621aafcf8b0fcddfb8ceb04b69caa4c79f4b955c9548ee8616290a538fcbdd3b2f1f1d35c6609e03d49de01db2b771a60e38fd7f277dd89b5f1a0abc0c31ae