#!/usr/bin/env bash set -Eeuo pipefail BUILDER_ROOT=${BUILDER_ROOT:-/opt/rust-package-builder} source "$BUILDER_ROOT/scripts/lib.sh" SOURCE_DIR=${1:?source path required} BINARY=${2:?FreeBSD binary required} PACKAGE_FILE=${3:?output package file required} PACKAGE_VERSION=${4:?OPNsense package version required} PLUGIN_ROOT="$BUILDER_ROOT/opnsense/plugin" FREEBSD_ABI=${FREEBSD_ABI:-FreeBSD:15:amd64} FREEBSD_ARCH=${FREEBSD_ARCH:-freebsd:15:x86:64} for tool in python3 sha256sum tar zstd file; do require_cmd "$tool" done [[ -f "$SOURCE_DIR/Cargo.toml" ]] || fatal "missing Cargo.toml in $SOURCE_DIR" [[ -f "$BINARY" ]] || fatal "missing cross-built binary: $BINARY" [[ -d "$PLUGIN_ROOT/src" ]] || fatal "missing embedded OPNsense plugin source: $PLUGIN_ROOT/src" binary_desc=$(file -b "$BINARY") [[ "$binary_desc" == *FreeBSD* ]] || fatal "refusing to package a non-FreeBSD binary: $binary_desc" WORK=$(mktemp -d "${PACKAGE_FILE}.work.XXXXXX") trap 'rm -rf "$WORK"' EXIT STAGE="$WORK/stage" META="$WORK/meta" ARCHIVE="$WORK/os-rustproxy.tar" PAYLOAD_LIST="$WORK/payload.list" mkdir -p "$STAGE/usr/local" "$META" "$(dirname "$PACKAGE_FILE")" cp -a "$PLUGIN_ROOT/src/." "$STAGE/usr/local/" mkdir -p "$STAGE/usr/local/sbin" install -m 0555 "$BINARY" "$STAGE/usr/local/sbin/rust-proxy" chmod 0555 "$STAGE/usr/local/etc/rc.d/rustproxy" chmod 0555 "$STAGE/usr/local/etc/rc.syshook.d/start/50-rustproxy.start" chmod 0555 "$STAGE/usr/local/opnsense/scripts/rustproxy/service.sh" POST_INSTALL_FILE="$PLUGIN_ROOT/+POST_INSTALL.pre" PRE_DEINSTALL_FILE="$PLUGIN_ROOT/+PRE_DEINSTALL.pre" python3 - "$STAGE" "$META" "$PACKAGE_VERSION" "$FREEBSD_ABI" "$FREEBSD_ARCH" \ "$POST_INSTALL_FILE" "$PRE_DEINSTALL_FILE" <<'PY' import hashlib import json 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(): files["/" + p.relative_to(stage).as_posix()] = "-" elif p.is_file(): files["/" + p.relative_to(stage).as_posix()] = 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 ( cd "$STAGE" find . \( -type f -o -type l \) -print | LC_ALL=C sort | sed 's#^\./##' > "$PAYLOAD_LIST" ) [[ -s "$PAYLOAD_LIST" ]] || fatal "OPNsense package payload is empty" 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" python3 - "$ARCHIVE" "$META/+MANIFEST" <<'PY' 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:] if any(m.isdir() for m in payload): raise SystemExit("directory entries are not allowed in OPNsense package payload") 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(f"package payload does not match +MANIFEST; missing={missing[:5]} extra={extra[:5]}") PY zstd -q -f -19 -T0 "$ARCHIVE" -o "$PACKAGE_FILE" zstd -dc "$PACKAGE_FILE" | tar -tf - >/dev/null log "created OPNsense package: $PACKAGE_FILE"