207 lines
6.9 KiB
Bash
Executable File
207 lines
6.9 KiB
Bash
Executable File
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
SOURCE_DIR="${RUSTPROXY_SOURCE_DIR:?RUSTPROXY_SOURCE_DIR is required}"
|
|
BINARY="${RUSTPROXY_BINARY:?RUSTPROXY_BINARY is required}"
|
|
OPNSENSE_ABI="${OPNSENSE_ABI:-26.7}"
|
|
FREEBSD_ABI="${FREEBSD_ABI:-FreeBSD:15:amd64}"
|
|
FREEBSD_ARCH="${FREEBSD_ARCH:-freebsd:15:x86:64}"
|
|
SOURCE_REVISION="${RUSTPROXY_SOURCE_REVISION:-0}"
|
|
SOURCE_COMMIT="${RUSTPROXY_SOURCE_COMMIT:-unknown}"
|
|
SOURCE_URL="${RUSTPROXY_SOURCE_URL:-https://git.linuxiarz.pl/gru/RustProxy}"
|
|
SOURCE_REF="${RUSTPROXY_SOURCE_REF:-HEAD}"
|
|
|
|
for tool in python3 sha256sum tar zstd file; do
|
|
command -v "$tool" >/dev/null 2>&1 || { echo "Missing tool: $tool" >&2; exit 1; }
|
|
done
|
|
|
|
[[ -f "$SOURCE_DIR/Cargo.toml" ]] || { echo "Missing Cargo.toml in $SOURCE_DIR" >&2; exit 1; }
|
|
[[ -f "$BINARY" ]] || { echo "Missing cross-built binary: $BINARY" >&2; exit 1; }
|
|
|
|
binary_desc="$(file -b "$BINARY")"
|
|
echo "$BINARY: $binary_desc"
|
|
if [[ "${ALLOW_NON_FREEBSD_BINARY:-0}" != "1" ]] && [[ "$binary_desc" != *FreeBSD* ]]; then
|
|
echo "Refusing to package a binary that is not identified as FreeBSD." >&2
|
|
exit 1
|
|
fi
|
|
|
|
app_version="$(awk -F '"' '/^version = "/ {print $2; exit}' "$SOURCE_DIR/Cargo.toml")"
|
|
[[ -n "$app_version" ]] || { echo "Cannot read app version" >&2; exit 1; }
|
|
|
|
if [[ "$SOURCE_REVISION" == "0" ]]; then
|
|
pkg_version="$app_version"
|
|
else
|
|
pkg_version="${app_version}_${SOURCE_REVISION}"
|
|
fi
|
|
|
|
work="$ROOT/dist/.package-work"
|
|
stage="$work/stage"
|
|
meta="$work/meta"
|
|
repo_dir="$ROOT/dist/packages/$OPNSENSE_ABI/amd64"
|
|
package_file="$repo_dir/os-rustproxy-${pkg_version}.pkg"
|
|
|
|
rm -rf "$work" "$ROOT/dist/packages"
|
|
mkdir -p "$stage/usr/local" "$meta" "$repo_dir"
|
|
|
|
# OPNsense plugin files map directly below /usr/local.
|
|
cp -a "$ROOT/opnsense-plugin/www/rustproxy/src/." "$stage/usr/local/"
|
|
mkdir -p "$stage/usr/local/sbin"
|
|
install -m 0555 "$BINARY" "$stage/usr/local/sbin/rust-proxy"
|
|
|
|
# Ensure executable hooks/scripts keep executable mode in the package.
|
|
chmod 0555 "$stage/usr/local/etc/rc.d/rustproxy"
|
|
chmod 0555 "$stage/usr/local/etc/rc.syshook.d/10-rustproxy.start"
|
|
chmod 0555 "$stage/usr/local/opnsense/scripts/rustproxy/service.sh"
|
|
|
|
POST_INSTALL_FILE="$ROOT/opnsense-plugin/www/rustproxy/+POST_INSTALL.pre"
|
|
PRE_DEINSTALL_FILE="$ROOT/opnsense-plugin/www/rustproxy/+PRE_DEINSTALL.pre"
|
|
|
|
python3 - "$stage" "$meta" "$pkg_version" "$FREEBSD_ABI" "$FREEBSD_ARCH" \
|
|
"$POST_INSTALL_FILE" "$PRE_DEINSTALL_FILE" <<'PY'
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
stage = Path(sys.argv[1])
|
|
meta_dir = Path(sys.argv[2])
|
|
version = sys.argv[3]
|
|
abi = sys.argv[4]
|
|
arch = sys.argv[5]
|
|
post_install = Path(sys.argv[6]).read_text(encoding="utf-8")
|
|
pre_deinstall = Path(sys.argv[7]).read_text(encoding="utf-8")
|
|
|
|
files = {}
|
|
flatsize = 0
|
|
for p in sorted(stage.rglob("*")):
|
|
if p.is_symlink():
|
|
rel = "/" + p.relative_to(stage).as_posix()
|
|
files[rel] = "-"
|
|
elif p.is_file():
|
|
rel = "/" + p.relative_to(stage).as_posix()
|
|
files[rel] = hashlib.sha256(p.read_bytes()).hexdigest()
|
|
flatsize += p.stat().st_size
|
|
|
|
common = {
|
|
"name": "os-rustproxy",
|
|
"version": version,
|
|
"origin": "opnsense/os-rustproxy",
|
|
"comment": "RustProxy reverse proxy service",
|
|
"desc": "RustProxy packaged as an OPNsense plugin with configurable HTTP, HTTPS/TLS and administration listeners.",
|
|
"maintainer": "local@localhost",
|
|
"www": "https://git.linuxiarz.pl/gru/RustProxy",
|
|
"prefix": "/usr/local",
|
|
"abi": abi,
|
|
"arch": arch,
|
|
"flatsize": flatsize,
|
|
"licenselogic": "single",
|
|
"licenses": ["UNKNOWN"],
|
|
"categories": ["www"],
|
|
"deps": {},
|
|
"options": {},
|
|
}
|
|
manifest = dict(common)
|
|
manifest["files"] = files
|
|
manifest["scripts"] = {
|
|
"post-install": post_install,
|
|
"pre-deinstall": pre_deinstall,
|
|
}
|
|
|
|
(meta_dir / "+COMPACT_MANIFEST").write_text(
|
|
json.dumps(common, separators=(",", ":"), ensure_ascii=False) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
(meta_dir / "+MANIFEST").write_text(
|
|
json.dumps(manifest, separators=(",", ":"), ensure_ascii=False) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
PY
|
|
|
|
archive="$work/os-rustproxy.tar"
|
|
payload_list="$work/payload.list"
|
|
rm -f "$archive" "$package_file" "$payload_list"
|
|
|
|
# FreeBSD pkg expects payload paths to match +MANIFEST exactly. The manifest
|
|
# uses absolute paths (/usr/local/...), so the tar members must also be
|
|
# absolute. Do not archive directory entries: pkg creates parent directories
|
|
# while extracting files, and an undeclared directory entry makes pkg add fail.
|
|
(
|
|
cd "$stage"
|
|
find . \( -type f -o -type l \) -print \
|
|
| LC_ALL=C sort \
|
|
| sed 's#^\./##' > "$payload_list"
|
|
)
|
|
|
|
[[ -s "$payload_list" ]] || { echo "Package payload is empty" >&2; exit 1; }
|
|
|
|
tar --format=ustar --owner=0 --group=0 --numeric-owner \
|
|
-cf "$archive" -C "$meta" +COMPACT_MANIFEST +MANIFEST
|
|
tar -P --format=ustar --owner=0 --group=0 --numeric-owner --no-recursion \
|
|
--transform='s,^,/,S' \
|
|
-rf "$archive" -C "$stage" -T "$payload_list"
|
|
|
|
# Validate the raw package archive before compression. This catches the exact
|
|
# class of errors that pkg(8) reports as "not specified in the manifest".
|
|
python3 - "$archive" "$meta/+MANIFEST" <<'PYVERIFY'
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import tarfile
|
|
|
|
archive = Path(sys.argv[1])
|
|
manifest = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8"))
|
|
expected = set(manifest.get("files", {}))
|
|
|
|
with tarfile.open(archive, "r:") as tf:
|
|
members = tf.getmembers()
|
|
|
|
if [m.name for m in members[:2]] != ["+COMPACT_MANIFEST", "+MANIFEST"]:
|
|
raise SystemExit("Package metadata must be the first two tar members")
|
|
|
|
payload = members[2:]
|
|
directories = [m.name for m in payload if m.isdir()]
|
|
if directories:
|
|
raise SystemExit(f"Directory entries are not allowed in payload: {directories[:5]}")
|
|
|
|
actual = {m.name for m in payload if m.isfile() or m.issym() or m.islnk()}
|
|
relative = sorted(name for name in actual if not name.startswith("/"))
|
|
if relative:
|
|
raise SystemExit(f"Payload contains non-absolute paths: {relative[:5]}")
|
|
|
|
missing = sorted(expected - actual)
|
|
extra = sorted(actual - expected)
|
|
if missing or extra:
|
|
raise SystemExit(
|
|
"Package payload does not match +MANIFEST; "
|
|
f"missing={missing[:5]} extra={extra[:5]}"
|
|
)
|
|
|
|
print(f"Verified package payload: {len(actual)} files, manifest paths match")
|
|
PYVERIFY
|
|
|
|
zstd -q -f -19 -T0 "$archive" -o "$package_file"
|
|
|
|
# Convenience stable path for direct deployment.
|
|
cp -f "$package_file" "$ROOT/dist/os-rustproxy-latest.pkg"
|
|
(cd "$repo_dir" && sha256sum "$(basename "$package_file")") > "$ROOT/dist/SHA256"
|
|
|
|
cat > "$ROOT/dist/BUILD-INFO.txt" <<INFO
|
|
app_version=$app_version
|
|
package_version=$pkg_version
|
|
source_url=$SOURCE_URL
|
|
source_ref=$SOURCE_REF
|
|
source_commit=$SOURCE_COMMIT
|
|
opnsense_series=$OPNSENSE_ABI
|
|
freebsd_abi=$FREEBSD_ABI
|
|
freebsd_arch=$FREEBSD_ARCH
|
|
package=$(basename "$package_file")
|
|
INFO
|
|
|
|
# Structural verification available on Linux.
|
|
zstd -dc "$package_file" | tar -tf - >/dev/null
|
|
rm -rf "$work"
|
|
echo "Package: $package_file"
|
|
echo "Latest: $ROOT/dist/os-rustproxy-latest.pkg"
|