Skip to content

Commit d06de32

Browse files
committed
CI: add FreeBSD 14 .pkg workflow and Makefile targets
1 parent bf69c99 commit d06de32

File tree

3 files changed

+322
-0
lines changed

3 files changed

+322
-0
lines changed

.github/workflows/freebsd-pkg.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: FreeBSD 14 .pkg
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches: [ "main" ]
7+
paths:
8+
- "src/**"
9+
- "packaging/**"
10+
- "scripts/build-and-package-freebsd.sh"
11+
- "ecli.spec"
12+
- "pyproject.toml"
13+
14+
jobs:
15+
build-freebsd-pkg:
16+
runs-on: ubuntu-latest
17+
name: "Build FreeBSD 14 .pkg"
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
with:
22+
fetch-depth: 0
23+
24+
- name: Build in FreeBSD 14 VM
25+
uses: vmactions/freebsd-vm@v1
26+
with:
27+
release: "14.1"
28+
usesh: true
29+
prepare: |
30+
set -eux
31+
env ASSUME_ALWAYS_YES=yes pkg update -f
32+
run: |
33+
set -eux
34+
sh ./scripts/build-and-package-freebsd.sh
35+
36+
- name: Find built .pkg
37+
id: find_pkg
38+
run: |
39+
set -e
40+
PKG_PATH="$(ls -1 releases/*/ecli-*.pkg | tail -n 1)"
41+
echo "pkg_path=$PKG_PATH" >> $GITHUB_OUTPUT
42+
43+
- name: Upload artifact (.pkg)
44+
uses: actions/upload-artifact@v4
45+
with:
46+
name: freebsd-pkg
47+
path: ${{ steps.find_pkg.outputs.pkg_path }}
48+
49+
- name: Commit .pkg into repository
50+
if: ${{ github.ref == 'refs/heads/main' }}
51+
run: |
52+
set -eux
53+
PKG="${{ steps.find_pkg.outputs.pkg_path }}"
54+
git config user.name "github-actions[bot]"
55+
git config user.email "github-actions[bot]@users.noreply.github.com"
56+
git add "$PKG"
57+
git commit -m "ci(freebsd): add built pkg artifact $PKG [skip ci]" || echo "No changes to commit"
58+
git push

Makefile

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ PACKAGE_VERSION := $(shell awk -F'"' '/^[[:space:]]*version[[:space:]]*=/ {print
1111

1212
.DEFAULT_GOAL := help
1313

14+
# ---- FreeBSD .pkg (via vmactions/freebsd-vm) -------------------------------
15+
FREEBSD_VM_IMAGE := ghcr.io/vmactions/freebsd-vm
16+
# tag order (can be changed if desired)
17+
FREEBSD_VM_TAGS ?= 14.2 14.1 14.0 14 latest
18+
19+
1420
# ---------------------------
1521
# Help
1622
# ---------------------------
@@ -70,3 +76,25 @@ package-rpm: clean
7076
package-rpm-docker:
7177
docker build -f docker/build-linux-rpm.Dockerfile -t ecli-rpm:alma9 .
7278
docker run --rm -v "$$(pwd):/app" -w /app ecli-rpm:alma9
79+
# ---------------------------
80+
# Packaging (PKG) — FreeBSD
81+
# ---------------------------
82+
83+
# Используй GitHub Actions для сборки .pkg во FreeBSD 14 VM
84+
.PHONY: package-freebsd-ci
85+
package-freebsd-ci:
86+
@echo "--> Triggering GitHub Actions workflow for FreeBSD .pkg build..."
87+
@echo " Open: https://github.com/ssobol77/ecli/actions/workflows/freebsd-pkg.yml"
88+
@echo " Click: 'Run workflow' (select branch: main)."
89+
@echo " Once completed, the .pkg will appear in artifacts and in releases/<version>/ (if autocommit is enabled)."
90+
@echo ""
91+
@echo "Optional via GitHub CLI:"
92+
@echo " gh workflow run freebsd-pkg.yml"
93+
94+
# Локально через Docker это не работает (образ vmactions/freebsd-vm недоступен для pull)
95+
.PHONY: package-freebsd-docker
96+
package-freebsd-docker:
97+
@echo "Local launch of FreeBSD VM via Docker is not possible:"
98+
@echo " ghcr.io/vmactions/freebsd-vm is not published for docker pull."
99+
@echo "Use: make package-freebsd-ci (build in GitHub Actions)."
100+
@exit 125
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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

Comments
 (0)