@@ -140,47 +140,17 @@ EOF
140140}
141141
142142run_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 "$@"
221230EOF
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
232239test_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