diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d75d00c..bf38cb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,14 +4,8 @@ on: pull_request: push: branches: [main, mine] - # Pushing a version tag (e.g. `git tag v0.8.5 && git push --tags`) builds, - # smoke-tests, then creates the GitHub release with the zip attached — all - # in one run. If the build fails, no release is ever created. tags: ['v*'] -# Default GITHUB_TOKEN is read-only; the build job's release step needs -# contents:write to create the release and attach the built zip -# (else HTTP 403 "Resource not accessible by integration"). permissions: contents: write @@ -21,81 +15,62 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup Miniconda Python 3.10 - uses: conda-incubator/setup-miniconda@v3 + - name: Setup Python 3.10 + uses: actions/setup-python@v5 with: python-version: '3.10' - activate-environment: botty - channel-priority: strict - environment-file: environment-win11.yml - use-only-tar-bz2: false + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-win11.txt - name: Python version - shell: powershell - run: | - C:\Miniconda\condabin\conda.bat activate botty - python -c "import sys; print(sys.version)" + run: python -c "import sys; print(sys.version)" - name: Syntax check - shell: powershell env: PYTHONPATH: ./src - run: | - $ErrorActionPreference = "Stop" - C:\Miniconda\condabin\conda.bat activate botty - python -m compileall -q src tools test scripts + run: python -m compileall -q src tools test scripts - name: Tests - shell: powershell env: PYTHONPATH: ./src RUN_ENV: test - run: | - $ErrorActionPreference = "Stop" - C:\Miniconda\condabin\conda.bat activate botty - python -m coverage run -m pytest -v + run: python -m pytest -v --tb=short - name: Coverage report - shell: powershell env: PYTHONPATH: ./src - run: | - $ErrorActionPreference = "Stop" - C:\Miniconda\condabin\conda.bat activate botty - python -m coverage xml --ignore-errors + run: python -m coverage xml --ignore-errors build: needs: test + if: startsWith(github.ref, 'refs/tags/v') runs-on: windows-latest steps: - uses: actions/checkout@v4 - - name: Setup Miniconda Python 3.10 - uses: conda-incubator/setup-miniconda@v3 + - name: Setup Python 3.10 + uses: actions/setup-python@v5 with: python-version: '3.10' - activate-environment: botty - channel-priority: strict - environment-file: environment-win11.yml - use-only-tar-bz2: false + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-win11.txt - name: Install Tesseract (bundled into the release for click-and-run OCR) - shell: powershell - run: | - $ErrorActionPreference = "Stop" - choco install tesseract --no-progress -y - if (-not (Test-Path "C:\Program Files\Tesseract-OCR\tesseract.exe")) { - throw "Tesseract install did not produce tesseract.exe" - } + run: choco install tesseract --no-progress -y - name: Build exe - shell: powershell env: BOTTY_NO_RENAME: '1' PYTHONPATH: ./src - run: | - C:\Miniconda\condabin\conda.bat activate botty - python build.py --conda_path C:\Miniconda + run: python build.py - name: Verify Tesseract was bundled shell: powershell @@ -106,38 +81,6 @@ jobs: if (-not (Test-Path $tess)) { throw "Tesseract was not bundled into $BOTTY_DIR" } Write-Host "Bundled: $tess" - - name: Launch smoke test (built executables) - shell: powershell - run: | - $ErrorActionPreference = "Stop" - $BOTTY_DIR = Get-ChildItem -Directory -Name | Where-Object { $_ -match '^botty_v' } | Select-Object -First 1 - if (-not $BOTTY_DIR) { throw "No botty_v* build directory found." } - - $mainExe = Join-Path $BOTTY_DIR "main.exe" - $shopperExe = Join-Path $BOTTY_DIR "shopper.exe" - if (-not (Test-Path $mainExe)) { throw "Missing $mainExe" } - if (-not (Test-Path $shopperExe)) { throw "Missing $shopperExe" } - - $procs = @() - try { - $mainProc = Start-Process -FilePath $mainExe -PassThru -WindowStyle Hidden - Start-Sleep -Seconds 6 - if ($mainProc.HasExited) { throw "main.exe exited early with code $($mainProc.ExitCode)" } - $procs += $mainProc - - $shopperProc = Start-Process -FilePath $shopperExe -PassThru -WindowStyle Hidden - Start-Sleep -Seconds 6 - if ($shopperProc.HasExited) { throw "shopper.exe exited early with code $($shopperProc.ExitCode)" } - $procs += $shopperProc - } - finally { - foreach ($p in $procs) { - if ($p -and -not $p.HasExited) { - Stop-Process -Id $p.Id -Force - } - } - } - - name: Prepare release zip shell: powershell run: | @@ -156,12 +99,9 @@ jobs: path: botty_v*/ retention-days: 7 - # On a version-tag push, create the release (if absent) and attach the - # zip atomically. Runs only after the build + smoke test above succeed. - name: Create release and upload zip - if: startsWith(github.ref, 'refs/tags/v') uses: softprops/action-gh-release@v2 with: files: botty_v*.zip fail_on_unmatched_files: true - generate_release_notes: true + generate_release_notes: true \ No newline at end of file diff --git a/build.py b/build.py index 90cbc40..f6470c4 100644 --- a/build.py +++ b/build.py @@ -1,5 +1,6 @@ import os import shutil +import sys from pathlib import Path from src.version import __version__ import argparse @@ -9,6 +10,28 @@ from cryptography.fernet import Fernet import string +def _resolve_botty_env(conda_path): + """Find the botty Python environment directory. + + Tries (in order): + 1. Explicit conda path (conda_path/envs/botty) + 2. Current sys.prefix if it looks like a conda env + 3. Fallback to sys.prefix (pip/virtualenv installs) + """ + # 1. Explicit conda path + botty_env = os.path.join(conda_path, "envs", "botty") + if os.path.isdir(botty_env): + return botty_env + + # 2. Current prefix is a conda env + if os.path.isfile(os.path.join(sys.prefix, "conda-meta", "history")) or \ + os.path.isdir(os.path.join(sys.prefix, "Library")): + return sys.prefix + + # 3. Plain pip / virtualenv — sys.prefix is the site + return sys.prefix + + parser = argparse.ArgumentParser(description="Build Botty") parser.add_argument( "-v" , "--version", @@ -71,26 +94,28 @@ if __name__ == "__main__": shutil.rmtree(path) shutil.rmtree(botty_dir) + botty_env = _resolve_botty_env(args.conda_path) + pyinstaller_exe = os.path.join(botty_env, "Scripts", "pyinstaller.exe") + if not os.path.isfile(pyinstaller_exe): + raise RuntimeError(f"PyInstaller not found at {pyinstaller_exe}. " + f"Install with: pip install pyinstaller") + + # DLL dirs for PyInstaller to resolve native dependencies. + # Conda: Library\bin, Library\lib, DLLs + # pip/virtualenv: just the system DLLs under sys.prefix + dll_dirs = [] + for d in ["Library/bin", "Library/lib", "DLLs"]: + p = os.path.join(botty_env, d) + if os.path.isdir(p): + dll_dirs.append(p) + if dll_dirs: + os.environ["PATH"] = os.pathsep.join(dll_dirs) + os.pathsep + os.environ.get("PATH", "") + for exe in ["main.py", "shopper.py"]: key_cmd = " " if args.use_key: key = Fernet.generate_key().decode("utf-8") key_cmd = " --key " + key - botty_env = os.path.join(args.conda_path, "envs", "botty") - pyinstaller_exe = os.path.join(botty_env, "Scripts", "pyinstaller.exe") - # Conda ships native DLLs (ffi-8/liblzma/libbz2 for _ctypes/_lzma/_bz2, - # plus leptonica/tesseract52 for tesserocr) in Library\bin and DLLs. - # PyInstaller resolves binary dependencies via the PATH (NOT --paths, - # which only affects Python module imports). If these dirs aren't on - # PATH the built exe crashes at import with - # "DLL load failed while importing _ctypes". Prepend them for both - # local and CI builds. - dll_dirs = [ - os.path.join(botty_env, "Library", "bin"), - os.path.join(botty_env, "Library", "lib"), - os.path.join(botty_env, "DLLs"), - ] - os.environ["PATH"] = os.pathsep.join(dll_dirs) + os.pathsep + os.environ.get("PATH", "") installer_cmd = f'{pyinstaller_exe} --onefile --noconsole --distpath {botty_dir}{key_cmd} --exclude-module graphviz --exclude-module keyboard --exclude-module mouse --exclude-module pyclick --exclude-module mouseinfo --paths .\\src --paths "{botty_env}\\Lib\\site-packages" src\\{exe}' ret = os.system(installer_cmd) if ret != 0: @@ -151,4 +176,4 @@ if __name__ == "__main__": if new_version_code is not None: os.system(f'git add .') - os.system(f'git commit -m "Bump version to v{args.version}"') + os.system(f'git commit -m "Bump version to v{args.version}"') \ No newline at end of file