Skip to content

Commit f14b42a

Browse files
authored
fix(ci): add wrapper venv for python cli as release version (#333)
1 parent 130b920 commit f14b42a

File tree

2 files changed

+98
-79
lines changed

2 files changed

+98
-79
lines changed

.github/workflows/release-cli.yml

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,36 +17,15 @@ jobs:
1717
runs-on: ubuntu-latest
1818
outputs:
1919
version: ${{ steps.version.outputs.VERSION }}
20-
strategy:
21-
matrix:
22-
include:
23-
- arch: x86_64
24-
python-arch: x64
25-
- arch: aarch64
26-
python-arch: x64
2720

2821
steps:
2922
- name: Checkout code
3023
uses: actions/checkout@v4
31-
24+
# dev builds run on host, prod uses PyInstaller on host
3225
- name: Set up Python
3326
uses: actions/setup-python@v4
3427
with:
3528
python-version: "3.11"
36-
architecture: ${{ matrix.python-arch }}
37-
38-
- name: Set up QEMU for cross-compilation
39-
if: matrix.arch == 'aarch64'
40-
uses: docker/setup-qemu-action@v3
41-
with:
42-
platforms: arm64
43-
44-
- name: Login to GHCR
45-
uses: docker/login-action@v3
46-
with:
47-
registry: ghcr.io
48-
username: ${{ github.actor }}
49-
password: ${{ secrets.GITHUB_TOKEN }}
5029

5130
- name: Install Poetry
5231
uses: snok/install-poetry@v1
@@ -79,16 +58,22 @@ jobs:
7958
sudo apt-get install -y ruby ruby-dev rubygems build-essential
8059
sudo gem install --no-document fpm
8160
82-
- name: Build CLI binary
61+
- name: Build CLI binary (PRs use dev mode / pushes build production)
8362
working-directory: cli
8463
env:
85-
NIXOPUS_DISABLE_DOCKER: ${{ github.event_name == 'pull_request' && '1' || '' }}
64+
NIXOPUS_DISABLE_DOCKER: "1"
8665
run: |
8766
chmod +x build.sh
8867
if [ ! -d "helpers" ]; then
8968
ln -s ../helpers helpers
9069
fi
91-
./build.sh --no-test --no-cleanup
70+
if [ "${{ github.event_name }}" = "pull_request" ]; then
71+
echo "Running dev-mode build (no PyInstaller)"
72+
./build.sh --dev --no-test --no-cleanup
73+
else
74+
echo "Running production build (PyInstaller)"
75+
./build.sh --no-test --no-cleanup --no-archive
76+
fi
9277
9378
- name: Prepare binary for packaging
9479
working-directory: cli
@@ -110,8 +95,14 @@ jobs:
11095
11196
- name: Set architecture variables
11297
run: |
113-
echo "ARCH=${{ matrix.arch }}" >> $GITHUB_ENV
114-
echo "PKG_ARCH=${{ matrix.arch == 'aarch64' && 'arm64' || 'amd64' }}" >> $GITHUB_ENV
98+
ARCH=$(uname -m)
99+
case "$ARCH" in
100+
x86_64) PKG_ARCH=amd64 ;;
101+
aarch64|arm64) PKG_ARCH=arm64 ;;
102+
*) PKG_ARCH=$ARCH ;;
103+
esac
104+
echo "ARCH=$ARCH" >> $GITHUB_ENV
105+
echo "PKG_ARCH=$PKG_ARCH" >> $GITHUB_ENV
115106
116107
- name: Create DEB package
117108
working-directory: cli
@@ -175,7 +166,7 @@ jobs:
175166
- name: Upload artifacts
176167
uses: actions/upload-artifact@v4
177168
with:
178-
name: nixopus-packages-${{ matrix.arch }}
169+
name: nixopus-packages-${{ env.ARCH }}
179170
path: |
180171
cli/*.deb
181172
cli/*.rpm

cli/build.sh

Lines changed: 79 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -140,47 +140,17 @@ EOF
140140
}
141141

142142
run_pyinstaller_build() {
143+
# Skip PyInstaller when in dev mode or explicitly disabled
144+
if [[ "$dev_mode" == true || "$skip_pyinstaller" == true ]]; then
145+
log_warning "Skipping PyInstaller build (dev mode or skip requested)"
146+
return
147+
fi
143148
# Ensure spec file exists even if manually deleted
144149
if [[ ! -f "$SPEC_FILE" ]]; then
145150
log_warning "Spec file missing; regenerating..."
146151
create_spec_file
147152
fi
148153

149-
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
150-
ARCH=$(uname -m)
151-
152-
if [[ "$OS" == "linux" ]] && command -v docker &> /dev/null && [[ -z "$NIXOPUS_DISABLE_DOCKER" ]]; then
153-
case $ARCH in
154-
x86_64)
155-
MANYLINUX_IMAGE="quay.io/pypa/manylinux2014_x86_64"
156-
PYTAG="cp311-cp311"
157-
;;
158-
aarch64|arm64)
159-
MANYLINUX_IMAGE="quay.io/pypa/manylinux2014_aarch64"
160-
PYTAG="cp311-cp311"
161-
;;
162-
*)
163-
MANYLINUX_IMAGE=""
164-
;;
165-
esac
166-
167-
# Use official PyInstaller image to ensure presence of libpython shared library
168-
PYI_IMAGE="ghcr.io/pyinstaller/pyinstaller:py3.11"
169-
log_info "Building with PyInstaller inside $PYI_IMAGE for reliable shared-lib Python..."
170-
docker run --rm -v "$(cd .. && pwd)":/work -w /work/cli "$PYI_IMAGE" bash -lc \
171-
"python3 -m pip install -U pip && \
172-
python3 -m pip install 'poetry==1.8.3' && \
173-
poetry config virtualenvs.in-project true && \
174-
poetry install --with dev && \
175-
poetry run pyinstaller --clean --noconfirm $SPEC_FILE" || {
176-
log_error "Dockerized build failed"
177-
exit 1
178-
}
179-
return
180-
181-
log_warning "Unsupported arch $ARCH for manylinux; building on host (may require newer glibc)"
182-
fi
183-
184154
log_info "Building with PyInstaller on host..."
185155
poetry run pyinstaller --clean --noconfirm $SPEC_FILE
186156
}
@@ -208,36 +178,73 @@ build_binary() {
208178

209179
BINARY_DIR_NAME="${APP_NAME}_${OS}_${ARCH}"
210180

211-
212-
if [[ -d "$BUILD_DIR/$APP_NAME" ]]; then
181+
mkdir -p "$BUILD_DIR"
182+
183+
# If PyInstaller produced the default folder, rename to OS/ARCH-specific name
184+
if [[ -d "$BUILD_DIR/$APP_NAME" ]]; then
213185
mv "$BUILD_DIR/$APP_NAME" "$BUILD_DIR/$BINARY_DIR_NAME"
186+
else
187+
log_warning "No PyInstaller output directory found; proceeding with dev-mode wrapper only"
188+
fi
214189

215-
216-
cat > "$BUILD_DIR/$APP_NAME" << EOF
190+
# Always (re)create the wrapper launcher
191+
cat > "$BUILD_DIR/$APP_NAME" << 'EOF'
217192
#!/bin/bash
193+
218194
# Nixopus CLI wrapper
219-
SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
220-
exec "\$SCRIPT_DIR/$BINARY_DIR_NAME/$APP_NAME" "\$@"
195+
set -euo pipefail
196+
197+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
198+
199+
# Detect OS/ARCH for bundled binary name
200+
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
201+
ARCH="$(uname -m)"
202+
case "$ARCH" in
203+
x86_64) ARCH="amd64" ;;
204+
aarch64|arm64) ARCH="arm64" ;;
205+
esac
206+
207+
APP_NAME="nixopus"
208+
BINARY_DIR_NAME="${APP_NAME}_${OS}_${ARCH}"
209+
BUNDLED_BIN="${SCRIPT_DIR}/${BINARY_DIR_NAME}/${APP_NAME}"
210+
211+
# If PyInstaller bundled binary exists, prefer it
212+
if [[ -x "$BUNDLED_BIN" ]]; then
213+
exec "$BUNDLED_BIN" "$@"
214+
fi
215+
216+
# Dev-mode fallback: use an isolated venv next to this wrapper
217+
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
218+
VENV_DIR="${SCRIPT_DIR}/.venv"
219+
PYTHON_BIN="${PYTHON_BIN:-python3}"
220+
221+
if [[ ! -d "$VENV_DIR" ]]; then
222+
echo "[INFO] Creating local virtualenv at $VENV_DIR"
223+
"$PYTHON_BIN" -m venv "$VENV_DIR"
224+
"$VENV_DIR/bin/python" -m pip install -U pip wheel
225+
# Install the CLI project in editable mode to use local sources
226+
"$VENV_DIR/bin/python" -m pip install -e "$PROJECT_DIR"
227+
fi
228+
229+
exec "$VENV_DIR/bin/python" -m app.main "$@"
221230
EOF
222-
chmod +x "$BUILD_DIR/$APP_NAME"
231+
chmod +x "$BUILD_DIR/$APP_NAME"
223232

233+
if [[ -d "$BUILD_DIR/$BINARY_DIR_NAME" ]]; then
224234
log_success "Binary directory built: $BUILD_DIR/$BINARY_DIR_NAME/"
225-
log_success "Wrapper script created: $BUILD_DIR/$APP_NAME"
226-
else
227-
log_error "Build failed - directory $BUILD_DIR/$APP_NAME not found"
228-
exit 1
229235
fi
236+
log_success "Wrapper script created: $BUILD_DIR/$APP_NAME"
230237
}
231238

232239
test_binary() {
233-
240+
234241
log_info "Testing binary..."
235242

236243
WRAPPER_PATH="$BUILD_DIR/$APP_NAME"
237-
244+
238245
if [[ -f "$WRAPPER_PATH" ]]; then
239246
chmod +x "$WRAPPER_PATH"
240-
247+
241248
if "$WRAPPER_PATH" --version; then
242249
log_success "Binary test passed"
243250
else
@@ -267,11 +274,19 @@ create_release_archive() {
267274
cd $BUILD_DIR
268275

269276

277+
# Collect files that actually exist
278+
FILES_TO_INCLUDE=("$APP_NAME")
279+
if [[ -d "$BINARY_DIR_NAME" ]]; then
280+
FILES_TO_INCLUDE+=("$BINARY_DIR_NAME")
281+
else
282+
log_warning "Bundled binary directory $BINARY_DIR_NAME not found; archiving wrapper only"
283+
fi
284+
270285
if [[ "$OS" == "darwin" || "$OS" == "linux" ]]; then
271-
tar -czf "${ARCHIVE_NAME}.tar.gz" "$BINARY_DIR_NAME" "$APP_NAME"
286+
tar -czf "${ARCHIVE_NAME}.tar.gz" "${FILES_TO_INCLUDE[@]}"
272287
log_success "Archive created: $BUILD_DIR/${ARCHIVE_NAME}.tar.gz"
273288
elif [[ "$OS" == "mingw"* || "$OS" == "cygwin"* || "$OS" == "msys"* ]]; then
274-
zip -r "${ARCHIVE_NAME}.zip" "$BINARY_DIR_NAME" "$APP_NAME"
289+
zip -r "${ARCHIVE_NAME}.zip" "${FILES_TO_INCLUDE[@]}"
275290
log_success "Archive created: $BUILD_DIR/${ARCHIVE_NAME}.zip"
276291
fi
277292

@@ -292,6 +307,8 @@ show_usage() {
292307
echo " --no-test Skip binary testing"
293308
echo " --no-archive Skip creating release archive"
294309
echo " --no-cleanup Skip cleanup of temporary files"
310+
echo " --dev Development mode (skip PyInstaller, wrapper uses local .venv)"
311+
echo " --skip-pyinstaller Skip PyInstaller build explicitly"
295312
echo " --help Show this help message"
296313
echo ""
297314
echo "Example:"
@@ -304,6 +321,8 @@ main() {
304321
local skip_test=false
305322
local skip_archive=false
306323
local skip_cleanup=false
324+
skip_pyinstaller=false
325+
dev_mode=false
307326

308327
while [[ $# -gt 0 ]]; do
309328
case $1 in
@@ -319,6 +338,15 @@ main() {
319338
skip_cleanup=true
320339
shift
321340
;;
341+
--skip-pyinstaller)
342+
skip_pyinstaller=true
343+
shift
344+
;;
345+
--dev)
346+
dev_mode=true
347+
skip_pyinstaller=true
348+
shift
349+
;;
322350
--help)
323351
show_usage
324352
exit 0

0 commit comments

Comments
 (0)