|
| 1 | +#!/bin/sh |
| 2 | +# ============================================================================== |
| 3 | +# Build and package ECLI into a native FreeBSD .pkg (FreeBSD 14.x) |
| 4 | +# |
| 5 | +# This script is intended to run INSIDE a FreeBSD 14 VM/container. |
| 6 | +# It: |
| 7 | +# 1) Installs build deps (Python 3.11, PyInstaller, etc.) |
| 8 | +# 2) Builds a one-file binary via PyInstaller (using ecli.spec if present) |
| 9 | +# 3) Stages files under /usr/local/... (FHS for FreeBSD) |
| 10 | +# 4) Creates a native .pkg with `pkg create` |
| 11 | +# |
| 12 | +# Output: releases/<version>/ecli-<version>.pkg |
| 13 | +# ============================================================================== |
| 14 | + |
| 15 | +set -eu |
| 16 | + |
| 17 | +PROJECT_ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" |
| 18 | +cd "$PROJECT_ROOT" |
| 19 | + |
| 20 | +PACKAGE_NAME="ecli" |
| 21 | +MAINTAINER="Siergej Sobolewski <s.sobolewski@hotmail.com>" |
| 22 | +HOMEPAGE="https://ecli.io" |
| 23 | +LICENSE="MIT" |
| 24 | +COMMENT="Terminal DevOps editor with AI and Git integration" |
| 25 | +CATEGORY="editors" # FreeBSD manifest 'categories' |
| 26 | + |
| 27 | +# ------------------------------------------------------------------------------ |
| 28 | +# 0) System prerequisites (FreeBSD 14) |
| 29 | +# ------------------------------------------------------------------------------ |
| 30 | +echo "==> Installing base build dependencies (FreeBSD 14)..." |
| 31 | +# fetch/curl:to install uv; ca_root_nss: TLS certs |
| 32 | +pkg install -y \ |
| 33 | + python311 py311-pip py311-setuptools py311-wheel py311-pyinstaller \ |
| 34 | + git gmake pkgconf ca_root_nss curl |
| 35 | + |
| 36 | +# ------------------------------------------------------------------------------ |
| 37 | +# 1) Userland package manager: uv |
| 38 | +# ------------------------------------------------------------------------------ |
| 39 | +if ! command -v uv >/dev/null 2>&1; then |
| 40 | + echo "==> Installing uv..." |
| 41 | + # FreeBSD имеет fetch в базе; используем его (можно и curl) |
| 42 | + fetch -o - https://astral.sh/uv/install.sh | sh |
| 43 | + export PATH="$HOME/.local/bin:$PATH" |
| 44 | +fi |
| 45 | +# Let's make sure that the UV is picked up |
| 46 | +export PATH="$HOME/.local/bin:$PATH" |
| 47 | + |
| 48 | +# ------------------------------------------------------------------------------ |
| 49 | +# 2) Python deps needed at analysis time for PyInstaller to bundle |
| 50 | +# (similar to deb/rpm: aiohttp stack + console libs) |
| 51 | +# ------------------------------------------------------------------------------ |
| 52 | +echo "==> Installing runtime Python deps via uv (system site, Python 3.11)..." |
| 53 | +uv pip install --system --python python3.11 \ |
| 54 | + aiohttp aiosignal yarl multidict frozenlist \ |
| 55 | + python-dotenv toml chardet \ |
| 56 | + pyperclip wcwidth pygments tato |
| 57 | + |
| 58 | +# ------------------------------------------------------------------------------ |
| 59 | +# 3) Determine version from pyproject.toml (use stdlib tomllib on 3.11) |
| 60 | +# ------------------------------------------------------------------------------ |
| 61 | +echo "==> Reading version from pyproject.toml..." |
| 62 | +VERSION="$(python3.11 - <<'PY' |
| 63 | +import tomllib |
| 64 | +with open("pyproject.toml","rb") as f: |
| 65 | + print(tomllib.load(f)["project"]["version"]) |
| 66 | +PY |
| 67 | +)" |
| 68 | +if [ -z "$VERSION" ]; then |
| 69 | + echo "ERROR: Could not read version from pyproject.toml" >&2 |
| 70 | + exit 1 |
| 71 | +fi |
| 72 | +echo "==> Version: $VERSION" |
| 73 | + |
| 74 | +RELEASES_DIR="releases/$VERSION" |
| 75 | +STAGING_ROOT="build/freebsd_pkg_staging" |
| 76 | +META_DIR="build/freebsd_pkg_meta" |
| 77 | +BIN_NAME="$PACKAGE_NAME" |
| 78 | +EXECUTABLE="" # will detect below |
| 79 | + |
| 80 | +# ------------------------------------------------------------------------------ |
| 81 | +# 4) Build one-file executable with PyInstaller |
| 82 | +# ------------------------------------------------------------------------------ |
| 83 | +echo "==> Cleaning previous artifacts" |
| 84 | +rm -rf build/ dist/ |
| 85 | + |
| 86 | +echo "==> Building executable with PyInstaller..." |
| 87 | +if [ -f "ecli.spec" ]; then |
| 88 | + # spec should already: |
| 89 | + # - add pathex=src |
| 90 | + # - embed config.toml |
| 91 | + # - force aiohttp stack + chardet |
| 92 | + # - include runtime hook packaging/pyinstaller/rthooks/force_imports.py |
| 93 | + pyinstaller ecli.spec --clean --noconfirm |
| 94 | +else |
| 95 | + # Backup route: we strictly collect everything we need |
| 96 | + pyinstaller main.py \ |
| 97 | + --name "$PACKAGE_NAME" \ |
| 98 | + --onefile --clean --noconfirm --strip \ |
| 99 | + --paths "src" \ |
| 100 | + --add-data "config.toml:." \ |
| 101 | + --hidden-import=ecli \ |
| 102 | + --hidden-import=dotenv --collect-all=dotenv \ |
| 103 | + --hidden-import=toml \ |
| 104 | + --hidden-import=aiohttp --collect-all=aiohttp \ |
| 105 | + --hidden-import=aiosignal --collect-all=aiosignal \ |
| 106 | + --hidden-import=yarl --collect-all=yarl \ |
| 107 | + --hidden-import=multidict --collect-all=multidict \ |
| 108 | + --hidden-import=frozenlist --collect-all=frozenlist \ |
| 109 | + --hidden-import=chardet --collect-all=chardet \ |
| 110 | + --hidden-import=pyperclip --collect-all=pyperclip \ |
| 111 | + --hidden-import=wcwidth --collect-all=wcwidth \ |
| 112 | + --hidden-import=pygments --collect-all=pygments \ |
| 113 | + --runtime-hook packaging/pyinstaller/rthooks/force_imports.py |
| 114 | +fi |
| 115 | + |
| 116 | +# Detect onefile result |
| 117 | +if [ -x "dist/$PACKAGE_NAME/$PACKAGE_NAME" ]; then |
| 118 | + EXECUTABLE="dist/$PACKAGE_NAME/$PACKAGE_NAME" |
| 119 | +elif [ -x "dist/$PACKAGE_NAME" ]; then |
| 120 | + EXECUTABLE="dist/$PACKAGE_NAME" |
| 121 | +fi |
| 122 | +if [ -z "$EXECUTABLE" ]; then |
| 123 | + echo "ERROR: PyInstaller output not found in dist/." >&2 |
| 124 | + exit 1 |
| 125 | +fi |
| 126 | +echo "==> Built: $EXECUTABLE" |
| 127 | + |
| 128 | +# ------------------------------------------------------------------------------ |
| 129 | +# 5) Stage files under /usr/local for FreeBSD packaging |
| 130 | +# ------------------------------------------------------------------------------ |
| 131 | +echo "==> Preparing staging layout under $STAGING_ROOT ..." |
| 132 | +rm -rf "$STAGING_ROOT" "$META_DIR" |
| 133 | +mkdir -p \ |
| 134 | + "$STAGING_ROOT/usr/local/bin" \ |
| 135 | + "$STAGING_ROOT/usr/local/share/applications" \ |
| 136 | + "$STAGING_ROOT/usr/local/share/icons/hicolor/256x256/apps" \ |
| 137 | + "$STAGING_ROOT/usr/local/share/doc/$PACKAGE_NAME" \ |
| 138 | + "$STAGING_ROOT/usr/local/man/man1" \ |
| 139 | + "$RELEASES_DIR" \ |
| 140 | + "$META_DIR" |
| 141 | + |
| 142 | +# Binary |
| 143 | +install -m 755 "$EXECUTABLE" "$STAGING_ROOT/usr/local/bin/$BIN_NAME" |
| 144 | + |
| 145 | +# Desktop entry (XDG .desktop correctly under /usr/local/share) |
| 146 | +if [ -f "packaging/linux/fpm-common/$PACKAGE_NAME.desktop" ]; then |
| 147 | + install -m 644 "packaging/linux/fpm-common/$PACKAGE_NAME.desktop" \ |
| 148 | + "$STAGING_ROOT/usr/local/share/applications/$PACKAGE_NAME.desktop" |
| 149 | +else |
| 150 | + cat > "$STAGING_ROOT/usr/local/share/applications/$PACKAGE_NAME.desktop" <<EOF |
| 151 | +[Desktop Entry] |
| 152 | +Name=ECLI |
| 153 | +Comment=Fast terminal code editor |
| 154 | +Exec=${PACKAGE_NAME} |
| 155 | +Icon=${PACKAGE_NAME} |
| 156 | +Terminal=true |
| 157 | +Type=Application |
| 158 | +Categories=Development;TextEditor; |
| 159 | +StartupNotify=false |
| 160 | +EOF |
| 161 | +fi |
| 162 | + |
| 163 | +# Icon |
| 164 | +if [ -f "img/logo_m.png" ]; then |
| 165 | + install -m 644 "img/logo_m.png" \ |
| 166 | + "$STAGING_ROOT/usr/local/share/icons/hicolor/256x256/apps/$PACKAGE_NAME.png" |
| 167 | +fi |
| 168 | + |
| 169 | +# Docs |
| 170 | +[ -f "LICENSE" ] && install -m 644 "LICENSE" "$STAGING_ROOT/usr/local/share/doc/$PACKAGE_NAME/LICENSE" |
| 171 | +[ -f "README.md" ] && install -m 644 "README.md" "$STAGING_ROOT/usr/local/share/doc/$PACKAGE_NAME/README.md" |
| 172 | + |
| 173 | +# Man page |
| 174 | +if [ ! -f "man/$PACKAGE_NAME.1" ]; then |
| 175 | + MANFILE="$STAGING_ROOT/usr/local/man/man1/$PACKAGE_NAME.1" |
| 176 | + DATE_STR="$(date +"%B %Y")" |
| 177 | + cat > "$MANFILE" <<EOF |
| 178 | +.TH ${PACKAGE_NAME} 1 "${DATE_STR}" "${PACKAGE_NAME} ${VERSION}" "User Commands" |
| 179 | +.SH NAME |
| 180 | +${PACKAGE_NAME} - Terminal code editor |
| 181 | +.SH SYNOPSIS |
| 182 | +.B ${PACKAGE_NAME} |
| 183 | +[\\fIOPTIONS\\fR] [\\fIFILE\\fR...] |
| 184 | +.SH DESCRIPTION |
| 185 | +${PACKAGE_NAME} is a fast terminal code editor. |
| 186 | +.SH OPTIONS |
| 187 | +\\fB--help\\fR Show help |
| 188 | +\\fB--version\\fR Show version |
| 189 | +.SH AUTHOR |
| 190 | +${MAINTAINER} |
| 191 | +.SH HOMEPAGE |
| 192 | +${HOMEPAGE} |
| 193 | +EOF |
| 194 | + gzip -f "$MANFILE" |
| 195 | +else |
| 196 | + install -m 644 "man/$PACKAGE_NAME.1" "$STAGING_ROOT/usr/local/man/man1/$PACKAGE_NAME.1" |
| 197 | + gzip -f "$STAGING_ROOT/usr/local/man/man1/$PACKAGE_NAME.1" |
| 198 | +fi |
| 199 | + |
| 200 | +# ------------------------------------------------------------------------------ |
| 201 | +# 6) Create +MANIFEST for pkg(8) and build .pkg |
| 202 | +# ------------------------------------------------------------------------------ |
| 203 | +ABI="$(pkg config ABI 2>/dev/null || echo 'FreeBSD:14:amd64')" |
| 204 | + |
| 205 | +MANIFEST_FILE="$META_DIR/+MANIFEST" |
| 206 | +cat > "$MANIFEST_FILE" <<EOF |
| 207 | +name: ${PACKAGE_NAME} |
| 208 | +version: ${VERSION} |
| 209 | +origin: ${CATEGORY}/${PACKAGE_NAME} |
| 210 | +comment: ${COMMENT} |
| 211 | +desc: | |
| 212 | + ${COMMENT} |
| 213 | +maintainer: ${MAINTAINER} |
| 214 | +www: ${HOMEPAGE} |
| 215 | +abi: ${ABI} |
| 216 | +prefix: / |
| 217 | +categories: |
| 218 | + - ${CATEGORY} |
| 219 | +licenses: |
| 220 | + - ${LICENSE} |
| 221 | +licenselogic: single |
| 222 | +EOF |
| 223 | + |
| 224 | +echo "==> Creating .pkg with pkg create..." |
| 225 | +# pkg create will take all files from the staging root, metadata from +MANIFEST |
| 226 | +pkg create -M "$MANIFEST_FILE" -r "$STAGING_ROOT" -o "$RELEASES_DIR" |
| 227 | + |
| 228 | +PKG_PATH="$(ls -1 "$RELEASES_DIR/${PACKAGE_NAME}-${VERSION}"*.pkg 2>/dev/null || true)" |
| 229 | +if [ -z "$PKG_PATH" ]; then |
| 230 | + echo "ERROR: pkg create did not produce a .pkg file" >&2 |
| 231 | + exit 1 |
| 232 | +fi |
| 233 | + |
| 234 | +echo "✅ DONE: $PKG_PATH" |
| 235 | +# Optional: list contents |
| 236 | +pkg info -F "$PKG_PATH" || true |
0 commit comments