diff --git a/.gitattributes b/.gitattributes index 721c5e1..39a0aff 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,5 @@ # patches are applied to upstream LF sources: never convert their line endings *.patch text eol=lf +*.sh text eol=lf +packaging/appimage/AppRun text eol=lf +*.desktop text eol=lf diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..71d1ddb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,270 @@ +name: release + +# A tag vX.Y.Z[-suffix] builds both packages and creates a DRAFT release (never +# published automatically). A manual run builds and tests everything but uploads +# nothing to Releases: the packages stay as workflow artifacts. +on: + push: + tags: ['v*'] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +env: + PYTHON_VERSION: '3.13' + +jobs: + version: + runs-on: ubuntu-22.04 + timeout-minutes: 5 + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - uses: actions/checkout@v4 + - name: Check the tag and derive the version + id: version + run: | + version=$(python3 packaging/release_meta.py version "$GITHUB_REF") + echo "version=$version" >> "$GITHUB_OUTPUT" + + tests: + needs: version + uses: ./.github/workflows/tests.yml + + tools-windows: + needs: version + runs-on: windows-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + - uses: msys2/setup-msys2@v2 + id: msys2 + with: + msystem: UCRT64 + update: true + install: >- + git unzip + mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-ninja + - name: Cache Windows programs + id: cache + uses: actions/cache@v4 + with: + path: bin + key: release-bin-windows-${{ hashFiles('packaging/third_party.json', 'packaging/build_raxml_windows.sh', 'packaging/patches/**') }} + - name: Fetch programs and build RAxML-NG + if: steps.cache.outputs.cache-hit != 'true' + shell: pwsh + run: | + $env:PHYMLGUI_BASH = Join-Path '${{ steps.msys2.outputs.msys2-location }}' 'usr\bin\bash.exe' + python scripts/fetch_third_party.py --platform windows --dest bin + - name: Install dependencies + run: pip install -r requirements.txt + - name: RAxML-NG parity run (Windows build) + run: python packaging/raxml_parity.py run --exe bin/raxml-ng.exe --out parity-windows + - uses: actions/upload-artifact@v4 + with: + name: bin-windows + path: bin + - uses: actions/upload-artifact@v4 + with: + name: parity-windows + path: parity-windows + + tools-linux: + needs: version + runs-on: ubuntu-22.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Install build tools and OpenMPI + run: > + sudo apt-get update && sudo apt-get install -y + openmpi-bin libopenmpi-dev build-essential autoconf automake libtool pkg-config + - name: Cache Linux programs + id: cache + uses: actions/cache@v4 + with: + path: bin + key: release-bin-linux-${{ hashFiles('packaging/third_party.json', 'packaging/build_phyml_linux.sh') }} + - name: Fetch programs and build PhyML / PhyML-MPI + if: steps.cache.outputs.cache-hit != 'true' + run: python scripts/fetch_third_party.py --platform linux --dest bin + - name: Install dependencies + run: pip install -r requirements.txt + - name: RAxML-NG parity run (official Linux build) + run: python packaging/raxml_parity.py run --exe bin/raxml-ng --out parity-linux + # tar keeps the executable bits that upload-artifact would drop + - name: Pack programs + run: tar -czf bin-linux.tar.gz bin + - uses: actions/upload-artifact@v4 + with: + name: bin-linux + path: bin-linux.tar.gz + - uses: actions/upload-artifact@v4 + with: + name: parity-linux + path: parity-linux + + raxml-parity: + needs: [tools-windows, tools-linux] + runs-on: ubuntu-22.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + - run: pip install biopython + - uses: actions/download-artifact@v4 + with: + name: parity-windows + path: parity-windows + - uses: actions/download-artifact@v4 + with: + name: parity-linux + path: parity-linux + - name: Compare the Windows build with the official Linux build + run: python packaging/raxml_parity.py compare parity-windows parity-linux + + package-windows: + needs: [version, tests, raxml-parity] + runs-on: windows-latest + timeout-minutes: 40 + env: + VERSION: ${{ needs.version.outputs.version }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + - run: pip install -r requirements.txt pyinstaller + - uses: actions/download-artifact@v4 + with: + name: bin-windows + path: bin + - name: Install Microsoft MPI (the bundled phyml-mpi.exe needs msmpi.dll) + run: | + python scripts/fetch_third_party.py --platform windows_runtime --dest mpi + Start-Process mpi/msmpisetup.exe -ArgumentList '-unattend' -Wait + - name: Build the zip + run: python packaging/package.py windows --version "$env:VERSION" --bin bin --out dist + - name: Smoke test + run: python packaging/smoke_test.py --package "dist/phymlGUI-$env:VERSION-windows-x64.zip" --version "$env:VERSION" + - uses: actions/upload-artifact@v4 + with: + name: package-windows + path: dist/phymlGUI-*-windows-x64.zip + + package-linux: + needs: [version, tests, raxml-parity] + runs-on: ubuntu-22.04 + timeout-minutes: 40 + env: + VERSION: ${{ needs.version.outputs.version }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Install Qt runtime libraries and OpenMPI + run: > + sudo apt-get update && sudo apt-get install -y + libegl1 libgl1 libxkbcommon0 libxkbcommon-x11-0 libdbus-1-3 libfontconfig1 libxcb-cursor0 + openmpi-bin + - run: pip install -r requirements.txt pyinstaller + - uses: actions/download-artifact@v4 + with: + name: bin-linux + - run: tar -xzf bin-linux.tar.gz + - name: Fetch the pinned appimagetool and runtime + run: python scripts/fetch_third_party.py --platform build_tools --dest tools + - name: Build the AppImage + run: > + python packaging/package.py linux --version "$VERSION" --bin bin --out dist + --appimagetool tools/appimagetool --runtime tools/runtime-x86_64 + - name: Smoke test (with OpenMPI) + run: python packaging/smoke_test.py --package "dist/phymlGUI-$VERSION-x86_64.AppImage" --version "$VERSION" --mpi + - name: Run in a clean ubuntu:22.04 container + run: | + mkdir check + cp "dist/phymlGUI-$VERSION-x86_64.AppImage" check/phymlgui.AppImage + python - <<'EOF' + import json, sys + sys.path.insert(0, 'tests/e2e') + from dataset_gen import make_dataset, write_fasta + names, _, unaligned = make_dataset() + write_fasta('check/unaligned.fasta', names, unaligned) + json.dump({'engine': 'phyml', 'datatype': 'nt', 'model': 'HKY85', + 'support_method': 'bootstrap', 'bootstrap_replicates': '4', + 'use_mpi': False}, open('check/config.json', 'w')) + EOF + docker run --rm -v "$PWD/check:/check" -w /check ubuntu:22.04 sh -c ' + set -e + ./phymlgui.AppImage --appimage-extract-and-run --version + ./phymlgui.AppImage --appimage-extract-and-run --file unaligned.fasta --config config.json + ls *_phyml_tree.txt' + - uses: actions/upload-artifact@v4 + with: + name: package-linux + path: dist/phymlGUI-*-x86_64.AppImage + + release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + needs: [version, package-windows, package-linux] + runs-on: ubuntu-22.04 + timeout-minutes: 15 + permissions: + contents: write + env: + VERSION: ${{ needs.version.outputs.version }} + TAG: ${{ github.ref_name }} + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + pattern: package-* + path: out + merge-multiple: true + - name: RAxML-NG sources and the Windows patch (AGPL-3.0) + run: | + url=$(python3 -c "import json; print(json.load(open('packaging/third_party.json'))['windows']['raxml']['url'])") + sha=$(python3 -c "import json; print(json.load(open('packaging/third_party.json'))['windows']['raxml']['sha256'])") + mkdir src && curl -fsSL -o src/raxml-ng_v2.0.3_source.zip "$url" + echo "$sha src/raxml-ng_v2.0.3_source.zip" | sha256sum -c - + python3 packaging/release_meta.py raxml-source src/raxml-ng_v2.0.3_source.zip \ + packaging/patches/raxml-ng-2.0.3-windows.patch out/raxml-ng-2.0.3-source+patch.tar.gz + - name: Checksums and release notes + run: | + python3 packaging/release_meta.py sha256sums out/SHA256SUMS \ + out/phymlGUI-*-windows-x64.zip out/phymlGUI-*-x86_64.AppImage out/raxml-ng-2.0.3-source+patch.tar.gz + python3 packaging/release_meta.py notes "$VERSION" notes.md + - name: Create or update the draft release + run: | + # vX.Y.Z-suffix (rc, beta) is a pre-release + prerelease=() + case "$VERSION" in *-*) prerelease=(--prerelease) ;; esac + files=(out/phymlGUI-*-windows-x64.zip out/phymlGUI-*-x86_64.AppImage + out/raxml-ng-2.0.3-source+patch.tar.gz out/SHA256SUMS) + if draft=$(gh release view "$TAG" --json isDraft -q .isDraft 2>/dev/null); then + if [ "$draft" != "true" ]; then + echo "::error::release $TAG is already published; not touching it" + exit 1 + fi + gh release upload "$TAG" "${files[@]}" --clobber + if [ ${#prerelease[@]} -gt 0 ]; then gh release edit "$TAG" "${prerelease[@]}"; fi + else + gh release create "$TAG" --draft "${prerelease[@]}" --verify-tag --title "phymlGUI $VERSION" \ + --notes-file notes.md "${files[@]}" + fi diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2794238..e487a1e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,15 +4,22 @@ on: push: branches: [master] pull_request: + workflow_call: jobs: test: - runs-on: windows-latest - timeout-minutes: 30 + runs-on: ${{ matrix.os }} + timeout-minutes: 40 strategy: fail-fast: false matrix: - python-version: ['3.11', '3.13'] + # Linux starts at 3.12: on 3.11 a C extension's True/False over-decref aborts + # the interpreter at exit (bool_dealloc); 3.12+ makes those objects immortal + include: + - {os: windows-latest, python-version: '3.11'} + - {os: windows-latest, python-version: '3.13'} + - {os: ubuntu-22.04, python-version: '3.12'} + - {os: ubuntu-22.04, python-version: '3.13'} env: QT_QPA_PLATFORM: offscreen steps: @@ -20,16 +27,25 @@ jobs: - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Install Qt runtime libraries, OpenMPI and build tools (Linux) + if: runner.os == 'Linux' + run: > + sudo apt-get update && sudo apt-get install -y + libegl1 libgl1 libxkbcommon0 libxkbcommon-x11-0 libdbus-1-3 libfontconfig1 libxcb-cursor0 + openmpi-bin libopenmpi-dev build-essential autoconf automake libtool pkg-config - name: Install dependencies run: pip install -r requirements.txt -r requirements-dev.txt - - name: Cache pinned IQ-TREE and MUSCLE + - name: Cache pinned third-party programs id: third-party uses: actions/cache@v4 with: path: bin - key: third-party-${{ hashFiles('packaging/third_party.json') }} - - name: Fetch pinned IQ-TREE and MUSCLE - if: steps.third-party.outputs.cache-hit != 'true' - run: python scripts/fetch_third_party.py --dest bin + key: third-party-${{ runner.os }}-${{ hashFiles('packaging/third_party.json', 'packaging/build_phyml_linux.sh') }} + - name: Fetch pinned IQ-TREE and MUSCLE (Windows) + if: runner.os == 'Windows' && steps.third-party.outputs.cache-hit != 'true' + run: python scripts/fetch_third_party.py --platform windows --only iqtree,muscle --dest bin + - name: Fetch IQ-TREE, MUSCLE and build PhyML (Linux) + if: runner.os == 'Linux' && steps.third-party.outputs.cache-hit != 'true' + run: python scripts/fetch_third_party.py --platform linux --only iqtree,muscle,phyml --dest bin - name: Tests run: python -m pytest -q -rs diff --git a/.gitignore b/.gitignore index a73ddc5..50ba777 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ phyml-mpi.exe /build /dist -phymlgui.spec +/phymlgui.spec __pycache__/ phyml_config.json phymlgui v1.1/ @@ -14,3 +14,5 @@ release.json release_resp.json asset_resp.json /bin/ +/build-bin/ +/build-parity-*/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2905a4b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,69 @@ +# Changelog + +All notable changes to phymlGUI are documented here. The format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning is +[SemVer](https://semver.org/). + +## [2.0.0] — 2026-10-31 + +A new window, a second engine, an interactive tree viewer and ready-to-run +packages for Windows and Linux with all analysis programs included. + +### Added +- IQ-TREE 2 engine: ModelFinder (`MFP`), ultrafast bootstrap with the + 1000-replicate minimum enforced, the chosen model and statistics in the window +- Tree viewer: rectangular, slanted and circular layouts, distance grid and + scale, support threshold highlighting, clade collapsing, rerooting on an + outgroup, majority-rule consensus from bootstrap trees, Robinson–Foulds + comparison, PNG/SVG/PDF export +- Run history (double click reopens a tree) and built-in and user parameter + presets +- Batch queue with per-file statuses, retry of the failed files and an Excel + (`.xlsx`) summary next to the CSV one +- Command line without the window: `--file`, `--batch`, `--config`, + `--version`, documented exit codes +- Linux support: XDG data folder, `mpirun` (OpenMPI), tools on `PATH` +- Release packages built by GitHub Actions: a Windows zip + (`phymlgui.exe` and `phymlgui-cli.exe`) and a Linux AppImage, each with + PhyML, IQ-TREE 2.4.0, MUSCLE 5.3 and RAxML-NG 2.0.3; `SHA256SUMS`; the + RAxML-NG sources and Windows patch attached to every release +- Native Windows build of RAxML-NG 2.0.3, checked against the official Linux + build (same best tree, likelihood, bootstrap replicates) +- Light and dark themes (following the system) with an accent colour + +### Changed +- Two-pane window: parameters, recent files, presets and history on the left; + the tree, log, statistics, summary and queue on the right +- Settings and history live in a data folder (next to the program when it is + writable, otherwise `%APPDATA%\phymlGUI` or `~/.config/phymlGUI`); settings + and history found in the working folder are copied there once +- All settings are checked before a run and every problem is listed at once +- The analysis core no longer depends on Qt; Cancel stops the whole process + tree, MPI ranks included + +### Fixed +- Crash when starting a run from an empty input +- IQ-TREE UFBoot and model errors +- Tree drawing: internal nodes at their own depth, non-overlapping supports, + circular layout of polytomies +- On Linux the serial PhyML is used when MPI is off, and child programs no + longer inherit the libraries of the package + +## [1.2] — 2026-10-24 + +### Added +- Batch mode for a folder of FASTA files with a CSV summary +- MPI autodetection, automatic core count and a live CPU monitor +- One-line statistics summary after each run + +### Changed +- Migrated the GUI from PyQt5 to PySide6 and split the code into modules + +## [1.1] — 2026-04-11 + +### Added +- MPI support, a single English/Russian program with `locales.json` + +## [1.0] — 2026-04-11 + +First release: a Windows GUI for the MUSCLE → PhyML pipeline. diff --git a/README.md b/README.md index 65b3a6a..1f56bc6 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,117 @@ -# phymlGUI: Automated Pipeline for Phylogenetic Analysis +# phymlGUI -[English Version](#english) | [Русская версия](#russian) +English | [Русский](README.ru.md) ---- +Open-source desktop tool for building maximum-likelihood phylogenetic trees from FASTA files. It aligns unaligned sequences with MUSCLE, runs **PhyML** (serial or MPI) or **IQ-TREE** (ModelFinder, ultrafast bootstrap) and shows the tree in an interactive viewer. Single files or whole folders, with a run history and parameter presets. Ready-to-run packages for Windows and Linux with all analysis programs included. English/Russian interface, dark and light themes. - -## 🇺🇸 English Version - -**phymlGUI** is a graphical interface for Windows that automates the construction of phylogenetic trees using the Maximum Likelihood method. - -### 🛠 Core Capabilities -* **Two Engines:** **PhyML** (serial or MPI) and **IQ-TREE** (automatic model selection with ModelFinder, ultrafast bootstrap) — switchable in Settings. -* **Integrated Pipeline:** Automatically detects unaligned sequences and runs **MUSCLE** before passing data to the engine. -* **Parallel Computing (MPI):** Supports execution via `mpirun`/`mpiexec` for multi-core processing; MPI binaries are detected automatically and misconfigured bootstrap runs are blocked with an explanation. -* **Batch Mode:** Process a whole folder of FASTA files in one run — a live per-file queue with statuses, retry of failed files, an N/M summary and automatic `phymlgui_summary.csv` / `.xlsx` reports. -* **Hardware Awareness:** Physical/logical core detection (the `np` field is pre-filled), plus a live CPU status bar showing both system load and the utilization of the analysis process tree. -* **Tree Viewer:** Rectangular / slanted / circular layouts, support-value threshold highlighting, clade collapsing, outgroup rerooting, majority-rule consensus from bootstrap trees, Robinson–Foulds topology comparison and PNG/SVG/PDF export. -* **Run History and Presets:** every run is recorded (double-click reloads its tree); built-in and user parameter presets. -* **Headless CLI:** `python phymlgui.py --file data.fasta` / `--batch folder/` runs the same pipeline without the GUI. Settings come from the data folder (not from a `phyml_config.json` in the current directory); `--config preset.json` is a JSON object laid over them. Exit codes: 0 success, 1 a file failed or the run was cancelled, 2 usage or input problem. -* **Data Sanitization / Conversion:** cleans sequence IDs, converts `FASTA` to `PHYLIP-relaxed`. -* **Themes:** light/dark (can follow the Windows system theme) with a selectable accent color. - -### ⚙️ Requirements -* **Executables:** `phyml.exe` (or MPI version) and `muscle.exe`. -* **Python packages:** Python 3.11+ with `PySide6`, `matplotlib`, `biopython` and `psutil` (`pip install -r requirements.txt`). -* **MPI Environment:** For parallel mode, **Microsoft MPI** is required (install `msmpisetup.exe` from the official Microsoft GitHub). -* **Settings and history:** stored next to the exe when that folder is writable, otherwise in `%APPDATA%\phymlGUI`. Set `PHYMLGUI_DATA_DIR` to use another folder. - -### 🧪 Tests -``` -pip install -r requirements.txt -r requirements-dev.txt -python scripts/fetch_third_party.py --dest bin # pinned IQ-TREE and MUSCLE (SHA256-verified) -python -m pytest +![phymlGUI: PhyML with MPI bootstrap on mammalian cytochrome b](docs/screenshots/main.png) + +## Features + +### 1. Analysis pipeline +- **Two engines:** PhyML 3.3 (serial or parallel through MPI) and IQ-TREE 2 (automatic model selection with ModelFinder, ultrafast bootstrap), switchable in Settings. +- **Alignment on the fly:** unaligned FASTA is detected and aligned with MUSCLE before the analysis; aligned files go straight to the engine. +- **Input checks before the run:** IDs are cleaned and checked for collisions, FASTA is converted to relaxed PHYLIP, and invalid settings (a missing program, an MPI-only build without MPI, bad numbers) are listed together before anything starts. +- **Parallel computing:** MPI through `mpiexec` (Microsoft MPI, Windows) or `mpirun` (OpenMPI, Linux); the core count is pre-filled from the physical cores. +- **Live CPU monitor:** the status bar shows the system load and the load of the analysis process tree. +- **Safe cancel:** Cancel stops the whole process tree (`mpiexec` and every PhyML rank). + +### 2. Tree viewer +- **Layouts:** rectangular, slanted and circular, with a distance grid and a scale in substitutions per site. +- **Supports:** bootstrap / aLRT / UFBoot values on branches, highlighting by a threshold. +- **Editing the view:** clade collapsing, rerooting on an outgroup, zoom and pan. +- **Comparison:** majority-rule consensus from bootstrap trees and Robinson–Foulds distance between two topologies. +- **Export:** PNG, SVG and PDF. + +![Dark theme, slanted layout](docs/screenshots/dark.png) + +### 3. IQ-TREE with ModelFinder +- **Model selection:** `MFP` lets ModelFinder choose the substitution model; the chosen model, log-likelihood, gamma shape and run time appear in the Statistics tab. +- **Ultrafast bootstrap:** UFBoot supports on the tree; fewer than 1000 replicates (the UFBoot minimum) are raised to 1000 with a warning. + +![IQ-TREE run with the statistics of the chosen model](docs/screenshots/iqtree.png) + +### 4. Batch mode, history and presets +- **Batch mode:** a whole folder of FASTA files in one run, with a live per-file queue, statuses, retry of the failed files and an N/M summary. +- **Reports:** `phymlgui_summary.csv` and `phymlgui_summary.xlsx` with the model, taxa, log-likelihood, gamma, Ts/Tv and run time of every file. +- **Run history:** every run is recorded with its parameters; a double click reopens its tree. +- **Presets:** built-in and user parameter presets, stored together with the engine. + +### 5. Command line +The same pipeline without the window, for scripts and servers: + +```bash +phymlgui-cli.exe --file data.fasta # Windows package +./phymlGUI-2.0.0-x86_64.AppImage --batch folder/ # Linux package +python phymlgui.py --file data.fasta --config preset.json ``` -Analysis binaries are looked up in `bin/` and in the variables `PHYMLGUI_PHYML`, `PHYMLGUI_MPIEXEC`, `PHYMLGUI_IQTREE`, `PHYMLGUI_MUSCLE`, `PHYMLGUI_RAXML` (full path to the executable). Tests that need a missing binary are skipped. Markers: `needs_phyml`, `needs_mpi`, `needs_iqtree`, `needs_muscle`, `slow`; CI runs plain `python -m pytest -q -rs`; the tests whose binaries are not installed there are skipped. Tests never touch your real settings: they use an isolated data folder. + +Settings come from the data folder; `--config` is a JSON object laid over them. Exit codes: `0` success, `1` a file failed or the run was cancelled, `2` a usage or input problem. --- - -## 🇷🇺 Русская версия - -**phymlGUI** — графический интерфейс для Windows, автоматизирующий построение филогенетических деревьев методом максимального правдоподобия. - -### 🛠 Основные возможности -* **Два движка:** **PhyML** (последовательно или MPI) и **IQ-TREE** (автоподбор модели ModelFinder, ультрабыстрый бутстреп) — переключение в настройках. -* **Интегрированный пайплайн:** Автоматическое определение невыровненных последовательностей и запуск **MUSCLE** перед анализом. -* **Параллельные вычисления (MPI):** Запуск через `mpirun`/`mpiexec`; MPI-бинарники определяются автоматически, некорректная конфигурация бутстрепа блокируется с пояснением. -* **Пакетный режим:** Обработка папки FASTA-файлов — живая очередь со статусами, повтор неудавшихся, сводка N/M и автоматические отчёты `phymlgui_summary.csv` / `.xlsx`. -* **Аппаратная осведомлённость:** Определение физических/логических ядер и живая строка состояния с загрузкой CPU — системы и дерева процессов анализа. -* **Просмотрщик деревьев:** Прямоугольная/сланцевая/круговая раскладки, подсветка поддержек по порогу, сворачивание клад, рерутинг по outgroup, консенсус большинства из бутстреп-деревьев, сравнение топологий по Робинсону–Фаулдсу, экспорт PNG/SVG/PDF. -* **История и пресеты:** каждый запуск записывается (двойной клик загружает его дерево); встроенные и пользовательские пресеты параметров. -* **Консольный режим:** `python phymlgui.py --file data.fasta` / `--batch папка/` — тот же пайплайн без GUI. Настройки берутся из папки данных (а не из `phyml_config.json` в текущей папке); `--config preset.json` — JSON-объект, накладываемый поверх них. Коды выхода: 0 — успех, 1 — файл не обработан или запуск отменён, 2 — ошибка аргументов или входных данных. -* **Очистка и конвертация:** санитарная очистка ID, перевод `FASTA` → `PHYLIP-relaxed`. -* **Темы:** светлая/тёмная (могут следовать системной Windows) с выбором акцентного цвета. - -### ⚙️ Требования -* **Программные файлы:** `phyml.exe` (или MPI-версия) и `muscle.exe`. -* **Python-зависимости:** Python 3.11+ с пакетами `PySide6`, `matplotlib`, `biopython` и `psutil` (`pip install -r requirements.txt`). -* **Среда MPI:** Для работы в параллельном режиме требуется установленный **Microsoft MPI** (инсталлятор `msmpisetup.exe` можно найти на официальном GitHub Microsoft). -* **Настройки и история:** хранятся рядом с exe, если папка доступна для записи, иначе в `%APPDATA%\phymlGUI`. Другую папку можно задать переменной `PHYMLGUI_DATA_DIR`. - -### 🧪 Тесты +## Download + +Ready-to-run packages are on the [Releases](https://github.com/adrobi/phymlGUI/releases) page; Python is not required. + +- **Windows 10/11, 64-bit:** `phymlGUI--windows-x64.zip`. Unpack the whole archive and run `phymlgui.exe` (`phymlgui-cli.exe` for the command line). PhyML needs [Microsoft MPI](https://github.com/microsoft/Microsoft-MPI/releases) (`msmpisetup.exe`), also for serial runs: the bundled PhyML is an MPI build. +- **Linux x86_64:** `phymlGUI--x86_64.AppImage`. Run `chmod +x phymlGUI-*.AppImage` and start it; without FUSE add `--appimage-extract-and-run`. Built on Ubuntu 22.04 (glibc 2.35 or newer). MPI mode needs OpenMPI: `sudo apt install openmpi-bin`. +- **Checksums:** `SHA256SUMS`, verify with `sha256sum -c SHA256SUMS`. + +Bundled programs (in `bin/`, licences in `licenses/`): + +| Program | Windows | Linux | +| --- | --- | --- | +| [PhyML](https://github.com/stephaneguindon/phyml) | 3.3, MPI build | 3.3.20260528, serial and MPI builds | +| [IQ-TREE](https://github.com/iqtree/iqtree2) | 2.4.0 | 2.4.0 | +| [MUSCLE](https://github.com/rcedgar/muscle) | 5.3 | 5.3 | +| [RAxML-NG](https://github.com/amkozlov/raxml-ng) | 2.0.3, native build | 2.0.3 | + +RAxML-NG is included for use from the command line; an engine for it in the window is planned. The Windows build is made from the official sources with a small patch; the sources and the patch are attached to every release (AGPL-3.0). + +Settings and history are kept next to the program when that folder is writable, otherwise in `%APPDATA%\phymlGUI` (Windows) or `~/.config/phymlGUI` (Linux, `$XDG_CONFIG_HOME`). `PHYMLGUI_DATA_DIR` selects another folder. + +## Installation & Quick Start (from source) + +Requires **Python 3.11+** (**3.12+** on Linux: with 3.11 the interpreter aborts on exit, `bool_dealloc`, because of a reference-count bug in a C extension). + +```bash +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -r requirements.txt +python scripts/fetch_third_party.py --dest bin # pinned, SHA256-verified programs +python phymlgui.py ``` -pip install -r requirements.txt -r requirements-dev.txt -python scripts/fetch_third_party.py --dest bin # закреплённые IQ-TREE и MUSCLE (проверка SHA256) + +Programs are looked up in `bin/` next to the app, then on `PATH`; paths can also be set in Settings. + +Run the tests: + +```bash +pip install -r requirements-dev.txt python -m pytest ``` -Бинарники анализа ищутся в `bin/` и в переменных `PHYMLGUI_PHYML`, `PHYMLGUI_MPIEXEC`, `PHYMLGUI_IQTREE`, `PHYMLGUI_MUSCLE`, `PHYMLGUI_RAXML` (полный путь к exe). Тесты, которым нужен отсутствующий бинарник, пропускаются. Маркеры: `needs_phyml`, `needs_mpi`, `needs_iqtree`, `needs_muscle`, `slow`; CI запускает просто `python -m pytest -q -rs`; тесты, для которых на CI нет бинарников, пропускаются. Тесты не трогают ваши настройки: они работают в изолированной папке данных. ---- +Tests that need a missing program are skipped; `PHYMLGUI_PHYML`, `PHYMLGUI_MPIEXEC`, `PHYMLGUI_IQTREE`, `PHYMLGUI_MUSCLE` and `PHYMLGUI_RAXML` point them to specific executables. The tests never touch your real settings. `scripts/make_screenshots.py` regenerates the screenshots from real runs. + +## Output files + +Next to each input file: + +- `*_aligned.fasta`: the MUSCLE alignment (for unaligned input) +- `*.phy`: the relaxed PHYLIP passed to the engine +- `*_phyml_tree.txt`, `*_phyml_stats.txt`, `*_phyml_boot_trees.txt`: PhyML results +- `*.treefile`, `*.iqtree`, `*.log`: IQ-TREE results +- `phymlgui_summary.csv`, `phymlgui_summary.xlsx`: one row per file -## 📂 Output Files / Выходные файлы +## Citation -* `*_aligned.fasta` — Aligned sequences / Выровненные последовательности. -* `*.phy` — Prepared PHYLIP format / Подготовленный формат PHYLIP. -* `*_phyml_tree.txt` — Resulting ML Tree (Newick) / Итоговое дерево (Newick). -* `*_phyml_stats.txt` — Analysis statistics / Статистика анализа. -* `phymlgui_summary.csv` — Per-file run summary (batch-friendly) / Сводка результатов по файлам. +If you use phymlGUI in published work, please cite the programs that did the analysis: -## Credits & Acknowledgments / Авторы и благодарности +- **PhyML:** Guindon S, Dufayard J-F, Lefort V, Anisimova M, Hordijk W, Gascuel O. New algorithms and methods to estimate maximum-likelihood phylogenies: assessing the performance of PhyML 3.0. *Systematic Biology* 59(3):307–321, 2010. +- **IQ-TREE 2:** Minh BQ, Schmidt HA, Chernomor O, et al. IQ-TREE 2: new models and efficient methods for phylogenetic inference in the genomic era. *Molecular Biology and Evolution* 37(5):1530–1534, 2020. +- **MUSCLE 5:** Edgar RC. Muscle5: high-accuracy alignment ensembles enable unbiased assessments of sequence homology and phylogeny. *Nature Communications* 13:6968, 2022. -* **PhyML:** The core phylogenetic analysis is powered by [PhyML](https://github.com/stephaneguindon/phyml), developed by Stephane Guindon and his team. - * *Citation:* Guindon S, et al. "New Algorithms and Methods to Estimate Maximum-Likelihood Phylogenies: Assessing the Performance of PhyML 3.0." Systematic Biology, 2010. -* **phymlGUI:** This wrapper/GUI was developed by Aleksey Drobin. +## License -### License -This project is licensed under the MIT License, but the included binaries (`phyml-mpi.exe`) are subject to their own respective licenses (GPL). \ No newline at end of file +MIT License — Copyright (c) 2026 Aleksey Drobin ([@adrobi](https://github.com/adrobi)). The bundled programs keep their own licences: PhyML and MUSCLE GPL-3.0, IQ-TREE GPL-2.0, RAxML-NG AGPL-3.0 (see `licenses/` in the packages). Changes are listed in [CHANGELOG.md](CHANGELOG.md). diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 0000000..4655e6a --- /dev/null +++ b/README.ru.md @@ -0,0 +1,117 @@ +# phymlGUI + +[English](README.md) | Русский + +Открытое десктопное приложение для построения филогенетических деревьев методом максимального правдоподобия по FASTA-файлам. Выравнивает невыровненные последовательности программой MUSCLE, запускает **PhyML** (последовательно или через MPI) или **IQ-TREE** (ModelFinder, ультрабыстрый бутстреп) и показывает дерево в интерактивном просмотрщике. Отдельные файлы или целые папки, история запусков и пресеты параметров. Готовые пакеты для Windows и Linux со всеми программами анализа внутри. Русский и английский интерфейс, тёмная и светлая темы. + +![phymlGUI: PhyML с MPI-бутстрепом на цитохроме b млекопитающих](docs/screenshots/main_ru.png) + +## Возможности + +### 1. Пайплайн анализа +- **Два движка:** PhyML 3.3 (последовательно или параллельно через MPI) и IQ-TREE 2 (автоподбор модели ModelFinder, ультрабыстрый бутстреп), переключение в настройках. +- **Выравнивание по ходу дела:** невыровненный FASTA распознаётся и выравнивается MUSCLE перед анализом; выровненные файлы сразу идут в движок. +- **Проверка до запуска:** ID очищаются и проверяются на совпадения, FASTA переводится в relaxed PHYLIP, а ошибки настроек (нет программы, MPI-сборка без MPI, неверные числа) показываются списком до старта. +- **Параллельные вычисления:** MPI через `mpiexec` (Microsoft MPI, Windows) или `mpirun` (OpenMPI, Linux); число ядер подставляется по физическим ядрам. +- **Монитор CPU:** в строке состояния видна загрузка системы и дерева процессов анализа. +- **Аккуратная отмена:** «Прервать» останавливает всё дерево процессов (`mpiexec` и каждый ранг PhyML). + +### 2. Просмотрщик деревьев +- **Раскладки:** прямоугольная, наклонная и круговая, с сеткой расстояний и шкалой в заменах на сайт. +- **Поддержки:** значения bootstrap / aLRT / UFBoot на ветвях, подсветка по порогу. +- **Работа с видом:** сворачивание клад, укоренение по outgroup, масштаб и перемещение. +- **Сравнение:** консенсус большинства из бутстреп-деревьев и расстояние Робинсона–Фаулдса между двумя топологиями. +- **Экспорт:** PNG, SVG и PDF. + +![Тёмная тема, наклонная раскладка](docs/screenshots/dark_ru.png) + +### 3. IQ-TREE с ModelFinder +- **Подбор модели:** `MFP` поручает выбор модели замен ModelFinder; выбранная модель, лог-правдоподобие, параметр гаммы и время работы показываются на вкладке «Статистика». +- **Ультрабыстрый бутстреп:** поддержки UFBoot на дереве; меньше 1000 реплик (минимум UFBoot) поднимается до 1000 с предупреждением. + +![Запуск IQ-TREE со статистикой выбранной модели](docs/screenshots/iqtree_ru.png) + +### 4. Пакетный режим, история и пресеты +- **Пакетный режим:** целая папка FASTA-файлов за один запуск — живая очередь со статусами, повтор неудавшихся файлов и сводка N/M. +- **Отчёты:** `phymlgui_summary.csv` и `phymlgui_summary.xlsx` с моделью, числом таксонов, лог-правдоподобием, гаммой, Ts/Tv и временем по каждому файлу. +- **История запусков:** каждый запуск записывается вместе с параметрами; двойной клик открывает его дерево. +- **Пресеты:** встроенные и пользовательские пресеты параметров, сохраняются вместе с движком. + +### 5. Командная строка +Тот же пайплайн без окна — для скриптов и серверов: + +```bash +phymlgui-cli.exe --file data.fasta # пакет для Windows +./phymlGUI-2.0.0-x86_64.AppImage --batch folder/ # пакет для Linux +python phymlgui.py --file data.fasta --config preset.json +``` + +Настройки берутся из папки данных; `--config` — JSON-объект, накладываемый поверх них. Коды выхода: `0` — успех, `1` — файл не обработан или запуск отменён, `2` — ошибка аргументов или входных данных. + +--- + +## Скачать + +Готовые пакеты лежат на странице [Releases](https://github.com/adrobi/phymlGUI/releases); Python не нужен. + +- **Windows 10/11, 64 бит:** `phymlGUI-<версия>-windows-x64.zip`. Распакуйте архив целиком и запустите `phymlgui.exe` (`phymlgui-cli.exe` — для командной строки). PhyML требует [Microsoft MPI](https://github.com/microsoft/Microsoft-MPI/releases) (`msmpisetup.exe`), в том числе для последовательного режима: встроенный PhyML собран с MPI. +- **Linux x86_64:** `phymlGUI-<версия>-x86_64.AppImage`. Выполните `chmod +x phymlGUI-*.AppImage` и запустите; без FUSE добавьте `--appimage-extract-and-run`. Собран на Ubuntu 22.04 (нужен glibc 2.35 или новее). Для MPI-режима нужен OpenMPI: `sudo apt install openmpi-bin`. +- **Контрольные суммы:** `SHA256SUMS`, проверка — `sha256sum -c SHA256SUMS`. + +Программы в комплекте (в `bin/`, лицензии — в `licenses/`): + +| Программа | Windows | Linux | +| --- | --- | --- | +| [PhyML](https://github.com/stephaneguindon/phyml) | 3.3, MPI-сборка | 3.3.20260528, последовательная и MPI-сборки | +| [IQ-TREE](https://github.com/iqtree/iqtree2) | 2.4.0 | 2.4.0 | +| [MUSCLE](https://github.com/rcedgar/muscle) | 5.3 | 5.3 | +| [RAxML-NG](https://github.com/amkozlov/raxml-ng) | 2.0.3, нативная сборка | 2.0.3 | + +RAxML-NG входит в комплект для работы из командной строки; движок для него в окне программы запланирован. Сборка для Windows сделана из официальных исходников с небольшим патчем; исходники и патч прикладываются к каждому релизу (AGPL-3.0). + +Настройки и история хранятся рядом с программой, если эта папка доступна для записи, иначе в `%APPDATA%\phymlGUI` (Windows) или `~/.config/phymlGUI` (Linux, `$XDG_CONFIG_HOME`). Другую папку задаёт переменная `PHYMLGUI_DATA_DIR`. + +## Установка и быстрый старт (из исходников) + +Нужен **Python 3.11+** (под Linux **3.12+**: с 3.11 интерпретатор аварийно завершается при выходе, `bool_dealloc`, из-за ошибки счётчика ссылок в C-расширении). + +```bash +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -r requirements.txt +python scripts/fetch_third_party.py --dest bin # закреплённые программы с проверкой SHA256 +python phymlgui.py +``` + +Программы ищутся в `bin/` рядом с приложением, затем в `PATH`; пути можно задать и в настройках. + +Запуск тестов: + +```bash +pip install -r requirements-dev.txt +python -m pytest +``` + +Тесты, которым нужна отсутствующая программа, пропускаются; переменные `PHYMLGUI_PHYML`, `PHYMLGUI_MPIEXEC`, `PHYMLGUI_IQTREE`, `PHYMLGUI_MUSCLE` и `PHYMLGUI_RAXML` указывают на конкретные исполняемые файлы. Тесты никогда не трогают ваши настоящие настройки. `scripts/make_screenshots.py` пересоздаёт скриншоты по настоящим запускам. + +## Выходные файлы + +Рядом с каждым входным файлом: + +- `*_aligned.fasta` — выравнивание MUSCLE (для невыровненного входа) +- `*.phy` — relaxed PHYLIP, переданный движку +- `*_phyml_tree.txt`, `*_phyml_stats.txt`, `*_phyml_boot_trees.txt` — результаты PhyML +- `*.treefile`, `*.iqtree`, `*.log` — результаты IQ-TREE +- `phymlgui_summary.csv`, `phymlgui_summary.xlsx` — по строке на файл + +## Цитирование + +Если вы используете phymlGUI в публикации, пожалуйста, цитируйте программы, которые выполнили анализ: + +- **PhyML:** Guindon S, Dufayard J-F, Lefort V, Anisimova M, Hordijk W, Gascuel O. New algorithms and methods to estimate maximum-likelihood phylogenies: assessing the performance of PhyML 3.0. *Systematic Biology* 59(3):307–321, 2010. +- **IQ-TREE 2:** Minh BQ, Schmidt HA, Chernomor O, et al. IQ-TREE 2: new models and efficient methods for phylogenetic inference in the genomic era. *Molecular Biology and Evolution* 37(5):1530–1534, 2020. +- **MUSCLE 5:** Edgar RC. Muscle5: high-accuracy alignment ensembles enable unbiased assessments of sequence homology and phylogeny. *Nature Communications* 13:6968, 2022. + +## Лицензия + +MIT License — Copyright (c) 2026 Aleksey Drobin ([@adrobi](https://github.com/adrobi)). Программы в комплекте распространяются по своим лицензиям: PhyML и MUSCLE — GPL-3.0, IQ-TREE — GPL-2.0, RAxML-NG — AGPL-3.0 (см. `licenses/` в пакетах). Изменения перечислены в [CHANGELOG.md](CHANGELOG.md). diff --git a/docs/screenshots/dark.png b/docs/screenshots/dark.png new file mode 100644 index 0000000..ba0cdac Binary files /dev/null and b/docs/screenshots/dark.png differ diff --git a/docs/screenshots/dark_ru.png b/docs/screenshots/dark_ru.png new file mode 100644 index 0000000..f4e4dcb Binary files /dev/null and b/docs/screenshots/dark_ru.png differ diff --git a/docs/screenshots/iqtree.png b/docs/screenshots/iqtree.png new file mode 100644 index 0000000..2862fe8 Binary files /dev/null and b/docs/screenshots/iqtree.png differ diff --git a/docs/screenshots/iqtree_ru.png b/docs/screenshots/iqtree_ru.png new file mode 100644 index 0000000..ed0f44e Binary files /dev/null and b/docs/screenshots/iqtree_ru.png differ diff --git a/docs/screenshots/main.png b/docs/screenshots/main.png new file mode 100644 index 0000000..546fd95 Binary files /dev/null and b/docs/screenshots/main.png differ diff --git a/docs/screenshots/main_ru.png b/docs/screenshots/main_ru.png new file mode 100644 index 0000000..894cfc7 Binary files /dev/null and b/docs/screenshots/main_ru.png differ diff --git a/docs/superpowers/plans/2026-10-31-release.md b/docs/superpowers/plans/2026-10-31-release.md new file mode 100644 index 0000000..26b9a1d --- /dev/null +++ b/docs/superpowers/plans/2026-10-31-release.md @@ -0,0 +1,158 @@ +# Релиз — план реализации + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** по тегу `v*` GitHub Actions собирает Windows-zip и Linux-AppImage со всеми программами анализа, проверяет их и создаёт черновик релиза. + +**Architecture:** приложение получает небольшой слой кроссплатформенности; сторонние программы описаны в одном манифесте по платформам и собираются/скачиваются скриптами из `packaging/`; один `release.yml` проходит тесты → инструменты → упаковка → дымовые тесты → черновик. + +**Tech Stack:** Python ≥ 3.11, PySide6, PyInstaller ≥ 6, appimagetool, MSYS2 UCRT64, autotools + OpenMPI, GitHub Actions (`windows-latest`, `ubuntu-22.04`). + +**Spec:** [docs/superpowers/specs/2026-10-31-release-design.md](../specs/2026-10-31-release-design.md) + +## Global Constraints + +- `phyml_core` не импортирует PySide6. +- Тесты не трогают настоящий конфиг/историю пользователя; весь набор зелёный на Windows и ubuntu-22.04. +- Задачи 1–3 и 6 (Linux) выполняются без субагентов (пожелание пользователя). +- Релиз по тегу — только **черновик**; `workflow_dispatch` ничего не публикует; push и теги — только с явного «да» пользователя. +- Каждый скачиваемый файл закреплён SHA256 в `packaging/third_party.json`; проверка до записи в `dest`. +- Ветка `release`; сообщения коммитов — английское предложение + `Co-Authored-By: Claude Opus 5.5 `. + +### Закреплённые значения (проверены 2026-10-31) + +| Платформа / инструмент | URL | SHA256 | Что берём | +|---|---|---|---| +| linux IQ-TREE 2.4.0 | `https://github.com/iqtree/iqtree2/releases/download/v2.4.0/iqtree-2.4.0-Linux-intel.tar.gz` | `49c58e26f192dd5e0045110cee0af151203dfa0bd79fa03d01fcfc3e44ca2f1b` | `iqtree-2.4.0-Linux-intel/bin/iqtree2` → `iqtree2` | +| linux MUSCLE 5.3 | `https://github.com/rcedgar/muscle/releases/download/v5.3/muscle-linux-x86.v5.3` | `318abeb951d786a3e2532714cc81ad3b3d8f79a2b517dc31316eeb5b694db2bc` | → `muscle` (chmod +x) | +| linux RAxML-NG 2.0.3 | `https://github.com/amkozlov/raxml-ng/releases/download/2.0.3/raxml-ng_v2.0.3_linux_x86_64.zip` | `d660cebe2a083de6c20d9354968803b8fd10be206f625be2393997d0abe98105` | `raxml-ng` | +| linux PhyML (исходники) | `https://github.com/stephaneguindon/phyml/archive/refs/tags/v3.3.20260528.tar.gz` | `42451c46de7cd01f8ca67d657766acf5b02f61027127bdda87efe3cdb422b297` | сборка → `phyml`, `phyml-mpi` | +| windows PhyML-MPI | `https://github.com/adrobi/phymlGUI/releases/download/v1.2/phymlGUI-1.2-win64.zip` | `4258578f2772fe5958513275cd7a64b6230d90795477b55ab156487bf0b08459` | `phymlGUI-1.2/phyml-mpi.exe` (SHA256 файла `4d1f57dd24ddc5718dd20b70dd2367688fa1fd5a8cf701a8ea7dafc6cb6107f7`) | +| windows RAxML-NG (исходники) | `https://github.com/amkozlov/raxml-ng/releases/download/2.0.3/raxml-ng_v2.0.3_source.zip` | `7cd3fa6b1de373777d688032c9ec6731078b67e6a812d7a1d779295d016843d2` | сборка по спайку → `raxml-ng.exe` | +| windows IQ-TREE, MUSCLE | как сейчас в `third_party.json` | как сейчас | как сейчас | + +## Review Focus + +1. **AppImage на машине без OpenMPI** — включённый MPI даёт понятную ошибку до старта, последовательный режим работает. Тест: Task 2 (`test_validate_mpirun_missing_on_linux`). +2. **Конфиг, перенесённый с Windows на Linux** (`mpirun_path: "mpiexec"`, пути `C:\...`) — окно открывается, `mpiexec` → `mpirun`, неверные пути дают ошибку «не найден», а не падение. Тест: Task 2 (`test_windows_config_loads_on_linux`). +3. **Только-чтение папки приложения в AppImage** — настройки уходят в `~/.config/phymlGUI` (или `$XDG_CONFIG_HOME`). Тест: Task 2 (`test_detect_falls_back_to_xdg_on_linux`). +4. **Пути с пробелами и кириллицей в Linux** (`/home/user/Мои данные/`). Тест: Task 3 — существующий `test_run_in_cyrillic_dir_with_spaces` должен проходить на ubuntu. +5. **Тег не того формата / повторный запуск по тегу** — workflow падает до сборки, черновик не дублируется. Тест: Task 8 (`test_release_tag_validation` для `packaging/release_meta.py`). + +--- + +## Файловая структура + +| Файл | Ответственность | +|---|---| +| `phyml_core/utils.py` | `exe_name`, `IS_WINDOWS`, `default_mpirun()` | +| `phyml_core/paths.py` | XDG-запасной вариант, `which` в `find_tool` | +| `phyml_ui/main_window.py`, `phyml_ui/themes.py` | «Открыть папку», тема на Linux | +| `packaging/third_party.json` | манифест по платформам | +| `packaging/build_phyml_linux.sh`, `packaging/build_raxml_windows.sh` | сборочные рецепты | +| `packaging/patches/raxml-ng-2.0.3-windows.patch` | копия patch'а из спайка (LF) | +| `packaging/licenses/` | тексты лицензий | +| `scripts/fetch_third_party.py` | `--platform`, шаг `build` | +| `packaging/phymlgui.spec` | PyInstaller onedir | +| `packaging/package.py` | сборка zip/AppDir, подстановка версии | +| `packaging/appimage/` | `AppRun`, `phymlgui.desktop` | +| `packaging/smoke_test.py` | дымовой тест готового пакета | +| `packaging/raxml_parity.py` | сверка RAxML-NG Windows vs Linux | +| `packaging/release_meta.py` | проверка тега, `SHA256SUMS`, текст релиза | +| `.github/workflows/tests.yml`, `.github/workflows/release.yml` | CI и релиз | + +--- + +### Task 1 (Linux, без субагентов): рецепт сборки PhyML под Linux + +**Files:** Create `packaging/build_phyml_linux.sh`. + +**Interfaces:** Produces: `bash packaging/build_phyml_linux.sh ` → `/phyml`, `/phyml-mpi` (исполняемые). Требует `autoconf automake libtool pkg-config libopenmpi-dev openmpi-bin`. + +- [ ] **Step 1:** Написать скрипт: распаковка во временную папку; `./autogen.sh`; `./configure --enable-phyml && make -j && cp src/phyml`; `make clean`; `./configure --enable-phyml-mpi && make -j && cp src/phyml-mpi` (без `--enable-native`, чтобы бинарник был переносимым). +- [ ] **Step 2: Проверить в WSL Ubuntu** (`wsl -d Ubuntu-26.04`, при необходимости `sudo apt install` — пароль вводит пользователь в своём терминале): оба бинарника собираются; `phyml --version` выводит версию; `mpirun -np 2 phyml-mpi -i tests/data/...phy -b 4` завершается с кодом 0 на PHYLIP из `tests/data` (сконвертировать `cytb` FASTA через `phyml_core.alignment.write_phylip`). +- [ ] **Step 3:** Если сборка требует правок исходников — остановиться и сообщить пользователю (риск из спецификации §8). +- [ ] **Step 4: Коммит** — `git commit -m "Add a Linux build recipe for PhyML and PhyML-MPI"` + +### Task 2 (Linux, без субагентов): кроссплатформенность приложения + +**Files:** Modify `phyml_core/utils.py`, `phyml_core/paths.py`, `phyml_core/settings.py`, `phyml_core/engines/{phyml,iqtree}.py`, `phyml_core/runner.py`, `phyml_ui/main_window.py`, `phyml_ui/themes.py`. Test: `tests/unit/test_platform.py`, дополнения в `tests/unit/test_paths.py`, `tests/unit/test_engine_phyml.py`, `tests/gui/test_main_window.py`. + +**Interfaces:** +- Produces: `utils.IS_WINDOWS: bool` (`os.name == 'nt'`); `utils.exe_name(base: str) -> str`; `utils.default_mpirun() -> str` (`'mpiexec'` на Windows, `'mpirun'` иначе); `AppPaths.find_tool` — после `bin/` пробует `shutil.which(name)`; `AppPaths.detect(..., platform: str | None = None)` — запасной вариант `APPDATA/phymlGUI` (win) или `$XDG_CONFIG_HOME/phymlGUI` → `~/.config/phymlGUI` (иначе). +- Движки: `PhyML.EXECUTABLE_NAMES` и `IqTree.EXECUTABLE_NAMES`, `runner.MUSCLE_NAMES` строятся через `exe_name`. Значение `mpirun_path` `'mpiexec'` на не-Windows при отсутствии `mpiexec` в PATH заменяется на `default_mpirun()` в `settings.from_disk`. + +- [ ] **Step 1: Падающие тесты** — `test_exe_name_per_platform` (monkeypatch `utils.IS_WINDOWS`), `test_find_tool_falls_back_to_path`, `test_detect_falls_back_to_xdg_on_linux` (`platform='linux'`, `env={'XDG_CONFIG_HOME': tmp}` → `tmp/phymlGUI`; без переменной → `~/.config/phymlGUI` с `HOME=tmp`), `test_validate_mpirun_missing_on_linux` (`use_mpi=True`, `mpirun_path='mpirun'`, `which` → None → `Issue('error','val_no_mpiexec',('mpirun',))`), `test_windows_config_loads_on_linux` (конфиг с `mpirun_path: "mpiexec"`, `phyml_path: "C:\\x\\phyml-mpi.exe"` → `from_disk` на linux даёт `mpirun`, `validate` даёт `val_no_exe` без исключений), `test_open_output_dir_uses_qdesktopservices_off_windows` (GUI, monkeypatch `QDesktopServices.openUrl`), `test_system_theme_without_winreg` (возвращает `'light'`/`'dark'` по палитре). +- [ ] **Step 2:** Запуск — FAIL. +- [ ] **Step 3:** Реализация по Interfaces; `themes.system_theme()` на не-Windows: `QGuiApplication.palette().window().color().lightness() < 128` → `'dark'` (если приложения ещё нет — `'light'`). +- [ ] **Step 4:** `python -m pytest -q -rs` на Windows — всё зелёное; тот же набор — в WSL Ubuntu (Task 3 даёт окружение). +- [ ] **Step 5: Коммит** — `git commit -m "Make the app portable to Linux: tool names, data dir, mpirun, open folder, theme"` + +### Task 3 (Linux, без субагентов): тесты на ubuntu-22.04 + +**Files:** Modify `.github/workflows/tests.yml`, `tests/conftest.py` (имена бинарников через `exe_name`), README (en/ru) — раздел про Linux. + +- [ ] **Step 1:** Матрица `os: [windows-latest, ubuntu-22.04] × python: ['3.11','3.13']`; на ubuntu — `sudo apt-get install -y libegl1 libgl1 libxkbcommon0 libdbus-1-3 libfontconfig1 openmpi-bin` и `python scripts/fetch_third_party.py --platform linux --only iqtree,muscle --dest bin`; `QT_QPA_PLATFORM=offscreen`. +- [ ] **Step 2:** Прогнать весь набор локально в WSL Ubuntu (venv, `pip install -r requirements.txt -r requirements-dev.txt`) — Expected: всё зелёное, пропуски только `needs_phyml`/`needs_mpi`. +- [ ] **Step 3: Коммит** — `git commit -m "Run the test suite on ubuntu-22.04 in CI"` + +### Task 4: манифест по платформам и сборочные шаги + +**Files:** Modify `packaging/third_party.json`, `scripts/fetch_third_party.py`, `tests/unit/test_fetch_third_party.py`. Create `packaging/build_raxml_windows.sh`, `packaging/patches/raxml-ng-2.0.3-windows.patch` (копия из `docs/superpowers/spikes/...`, LF), `packaging/licenses/{PhyML,IQ-TREE,MUSCLE}-GPL-3.0.txt`, `RAxML-NG-AGPL-3.0.txt`, `README.md` (что и откуда). + +**Interfaces:** +- `third_party.json`: `{"windows": {...}, "linux": {...}}`; у записи поля `version`, `url`, `sha256` и одно из `extract` (zip/tar.gz → {член: имя}), `save_as`, `build` (`{"script": "packaging/build_….sh"}`, скрипт получает путь к скачанному архиву и `dest`); `mode: "755"` для Linux-файлов. +- `fetch(name, spec, dest) -> list[str]` — как сейчас + tar.gz + `build` + `mode`; `main(argv)` получает `--platform {windows,linux}` (по умолчанию — текущая). +- `build_raxml_windows.sh ` — рецепт спайка (UCRT64, 4 флага CMake, `git apply` patch'а) → `/raxml-ng.exe`. + +- [ ] **Step 1: Падающие тесты** — `test_fetch_extracts_tar_gz_member`, `test_fetch_runs_build_script` (скрипт-заглушка пишет файл в dest), `test_fetch_sets_mode_on_posix` (skip на Windows), `test_manifest_has_both_platforms_and_valid_hashes` (каждая запись: 64 hex, https-URL). +- [ ] **Step 2:** FAIL → реализация → PASS. +- [ ] **Step 3: Проверка рецепта RAxML-NG** локально в MSYS2: `bash packaging/build_raxml_windows.sh /tmp/out` → `raxml-ng.exe --version` выводит `2.0.3`. +- [ ] **Step 4: Коммит** — `git commit -m "Describe third-party tools per platform and add build steps"` + +### Task 5: PyInstaller и Windows-zip + +**Files:** Create `packaging/phymlgui.spec`, `packaging/package.py`, `packaging/smoke_test.py`; Modify `.gitignore` (убрать `phymlgui.spec`), `requirements-dev.txt` (`pyinstaller>=6`). Test: `tests/unit/test_package.py`. + +**Interfaces:** +- `package.py`: `version_from_tag(tag: str) -> str` (`v2.0.0` → `2.0.0`, `v2.0.0-rc1` → `2.0.0-rc1`, иначе `ValueError`); `stamp_version(version, init_path)` — переписывает `__version__` во временной копии для сборки; `build_windows(version, bin_dir, out_dir) -> str` (путь к zip `phymlGUI-{version}-windows-x64.zip`, раскладка из спецификации §4); `build_appdir(version, bin_dir, out_dir) -> str` (Task 6). +- `smoke_test.py`: `main(argv)` — `--package ` распаковывает во временную папку и выполняет: `--version` содержит версию; CLI `--file ` с `engine=phyml` (последовательный, `support_method=-4`) и с `engine=iqtree` → exit 0 и файл дерева существует; offscreen-запуск окна (`--smoke-gui`, открыть и закрыть через `QTimer`) → exit 0; `--mpi` дополнительно запускает PhyML-MPI (только Linux). +- В приложение добавляется `--version` (CLI печатает `phymlGUI X.Y.Z`) и скрытый `--smoke-gui`. + +- [ ] **Step 1: Падающие тесты** — `test_version_from_tag` (три случая выше + `'2.0.0'` без `v` → `ValueError`), `test_stamp_version_does_not_touch_repo_file`, `test_cli_version_flag`. +- [ ] **Step 2:** FAIL → реализация → PASS. +- [ ] **Step 3:** Локально: `python scripts/fetch_third_party.py --platform windows --dest bin` → `python packaging/package.py windows --version 0.0.0-dev --bin bin --out dist` → `python packaging/smoke_test.py --package dist/phymlGUI-0.0.0-dev-windows-x64.zip` — Expected: все проверки OK. +- [ ] **Step 4: Коммит** — `git commit -m "Package the Windows zip with PyInstaller and smoke-test it"` + +### Task 6 (Linux, без субагентов): AppImage + +**Files:** Create `packaging/appimage/AppRun`, `packaging/appimage/phymlgui.desktop`; Modify `packaging/package.py` (`build_appdir`, `build_appimage(version, bin_dir, out_dir, appimagetool) -> str`), `packaging/smoke_test.py` (AppImage: `--appimage-extract-and-run`). + +- [ ] **Step 0:** Закрепить `appimagetool`: взять последний **тегированный** (не `continuous`) релиз `AppImage/appimagetool` x86_64 через GitHub API, скачать, посчитать SHA256 и записать в `third_party.json` → `build_tools.appimagetool` (`url`, `sha256`, `mode: "755"`). +- [ ] **Step 1:** `AppRun`: `exec "$APPDIR/usr/bin/phymlgui" "$@"` с `HERE`-определением; `.desktop`: `Name=phymlGUI`, `Exec=phymlgui`, `Icon=phymlgui`, `Categories=Science;Biology;`. +- [ ] **Step 2:** В WSL Ubuntu: `fetch --platform linux`, PyInstaller, `build_appimage` (appimagetool, закреплённый по SHA256 в `third_party.json` секции `build_tools`), затем `smoke_test.py --package dist/phymlGUI-0.0.0-dev-x86_64.AppImage --mpi` — Expected: все проверки OK. +- [ ] **Step 3:** Проверка в чистом контейнере `ubuntu:22.04` (в CI, Task 8) — локально, если в WSL есть docker; иначе отметить, что проверяется в CI. +- [ ] **Step 4: Коммит** — `git commit -m "Build and smoke-test the Linux AppImage"` + +### Task 7: сверка RAxML-NG + +**Files:** Create `packaging/raxml_parity.py`; Test: `tests/unit/test_raxml_parity.py`. + +**Interfaces:** `compare(win_dir: str, lin_dir: str) -> list[str]` — список расхождений (пустой = совпало) по правилам спайка: lnL `|Δ| < 1e-3`, RF лучших деревьев = 0, бутстреп-реплики одинаковы, lnL на фиксированном дереве совпадает до 6 знаков; `main(argv)` — `--win --linux --data tests/data` запускает оба на одинаковых входах (`--seed 42`), возвращает 1 при расхождениях. + +- [ ] **Step 1: Падающие тесты** `compare` на синтетических файлах вывода (совпадают → `[]`; другой bestTree → 1 расхождение с именем файла). +- [ ] **Step 2:** FAIL → реализация → PASS. Локальная проверка: Windows-сборка из Task 4 против Linux-бинарника в WSL — `[]`. +- [ ] **Step 3: Коммит** — `git commit -m "Add the RAxML-NG Windows vs Linux parity check"` + +### Task 8: release.yml и черновик релиза + +**Files:** Create `.github/workflows/release.yml`, `packaging/release_meta.py`; Test: `tests/unit/test_release_meta.py`. + +**Interfaces:** `release_meta.py`: `check_tag(tag: str) -> str` (через `version_from_tag`); `write_sha256sums(files: list[str], out: str) -> None` (формат `sha256␠␠имя`); `release_notes(version: str, manifest: dict) -> str` (список вложенных инструментов с версиями, требование MS-MPI/OpenMPI для MPI). + +- [ ] **Step 1: Падающие тесты** — `test_release_tag_validation` (`v2.0.0` ok; `release-1`, `v2`, `2.0.0` → ошибка), `test_sha256sums_format`, `test_release_notes_lists_tools`. +- [ ] **Step 2:** FAIL → реализация → PASS. +- [ ] **Step 3:** `release.yml` по спецификации §5: `on: push: tags: ['v*']` и `workflow_dispatch`; задания `tests` (`uses: ./.github/workflows/tests.yml`, для этого добавить `on: workflow_call` в tests.yml), `tools-windows`, `tools-linux` (ubuntu-22.04), `raxml-parity` (needs both; скачивает артефакты; Linux-бинарник запускается на ubuntu, Windows — на windows, вывод сравнивается в третьем шаге), `package-windows`, `package-linux` (+ шаг `docker run --rm -v ... ubuntu:22.04` с `--appimage-extract-and-run`), `release` (`if: startsWith(github.ref, 'refs/tags/v')`, `gh release create "$TAG" --draft --title ... --notes-file ...` + файлы + `SHA256SUMS` + `raxml-ng-2.0.3-source+patch.tar.gz`; если черновик для тега уже есть — `gh release upload --clobber` вместо create); `permissions: contents: write` только у `release`; `concurrency: release-${{ github.ref }}`; кэш инструментов по `hashFiles('packaging/third_party.json')`; у всех заданий `timeout-minutes`. +- [ ] **Step 4:** Локально: `python -m pytest -q -rs` — зелёный. +- [ ] **Step 5: Коммит** — `git commit -m "Add the release workflow: tools, packages, smoke tests, draft release"` +- [ ] **Step 6: Только после «да» пользователя** — push ветки `release`, ручной запуск `workflow_dispatch` (Expected: все задания зелёные, черновика нет); затем, по отдельному «да», тег `v2.0.0-rc1` → черновик релиза с zip, AppImage, `SHA256SUMS`, исходниками RAxML-NG (критерий 3). diff --git a/docs/superpowers/specs/2026-10-31-release-design.md b/docs/superpowers/specs/2026-10-31-release-design.md new file mode 100644 index 0000000..ac283c8 --- /dev/null +++ b/docs/superpowers/specs/2026-10-31-release-design.md @@ -0,0 +1,134 @@ +# Подпроект «Релиз» — дизайн + +Дата: 2026-10-31 · Статус: на ревью · Опирается на: [«Фундамент»](2026-10-25-foundation-design.md), [спайк RAxML-NG](../spikes/2026-10-24-raxml-ng-windows/README.md) + +## 1. Цель и решения + +Релиз phymlGUI собирается в GitHub Actions по тегу и даёт два самодостаточных +пакета: zip для Windows x64 и AppImage для Linux x86-64. В обоих пакетах уже +есть все анализ-программы. + +**Решения пользователя:** Linux-пакет — AppImage; внутри оба PhyML +(последовательный и MPI), MPI на Linux — системный OpenMPI; релиз создаётся по +тегу `v*` как **черновик**, публикует его пользователь; RAxML-NG собирается и +кладётся в пакеты уже сейчас; один workflow `release.yml` (подход A); упаковщик +— PyInstaller. Задачи по Linux выполняются без субагентов. + +## 2. Перенос приложения на Linux + +- `phyml_core/utils.exe_name(base) -> str`: `base + '.exe'` на Windows, `base` + иначе. Движки (`phyml-mpi`/`phyml`, `iqtree2`) и runner (`muscle`) ищут + инструменты через него. +- `AppPaths.find_tool`: после `bin/` — поиск через `shutil.which` по тем же + именам. +- «Открыть папку результатов»: `os.startfile` на Windows, + `QDesktopServices.openUrl(QUrl.fromLocalFile(...))` на Linux. +- Тема «как в системе» на Linux: светлота палитры Qt; акцент — `green`. +- Папка данных: правило «папка приложения, если в неё можно писать» остаётся; + запасной вариант — `%APPDATA%\phymlGUI` на Windows и + `$XDG_CONFIG_HOME/phymlGUI` (по умолчанию `~/.config/phymlGUI`) на Linux. + AppImage монтируется только для чтения, поэтому там всегда работает запасной + вариант. +- MPI: по умолчанию `mpiexec` на Windows и `mpirun` на Linux. Значение + `mpiexec` из конфига, перенесённого с Windows, на Linux заменяется на + `mpirun`, если `mpiexec` не найден. Без OpenMPI проверка перед запуском даёт + понятную ошибку (`val_no_mpiexec` с именем команды). +- `tests.yml`: к заданию на `windows-latest` добавляется `ubuntu-22.04` + (Python 3.11 и 3.13, offscreen Qt, системные пакеты для Qt). + +## 3. Сторонние инструменты + +`packaging/third_party.json` получает секции `windows` и `linux`; у каждого +инструмента — версия, источник, SHA256 и способ получения (`download`, +`extract`, `build`). + +| Инструмент | Windows | Linux | +|---|---|---| +| IQ-TREE 2.4.0 | официальный zip (закреплён) | официальный архив Linux x86-64 | +| MUSCLE 5.3 | официальный exe (закреплён) | официальный бинарник linux x86 | +| RAxML-NG 2.0.3 | сборка MSYS2 UCRT64 по рецепту спайка + patch | официальный zip | +| PhyML | `phyml-mpi.exe` из релиза phymlGUI v1.2, закреплён по SHA256 | сборка из закреплённого тега исходников: `phyml` и `phyml-mpi` (OpenMPI) | + +- Рецепты сборки — `packaging/build_raxml_windows.sh`, + `packaging/build_phyml_linux.sh`; их можно запускать локально. +- `scripts/fetch_third_party.py --platform {windows,linux} --dest bin` + выполняет загрузки и сборки по манифесту; проверка SHA256 до записи в `dest` + сохраняется. +- Лицензии: `packaging/licenses/` (GPL для PhyML, IQ-TREE, MUSCLE; AGPL-3.0 для + RAxML-NG) кладётся в оба пакета. + +## 4. Упаковка + +- `packaging/phymlgui.spec` (в git) — PyInstaller, режим onedir, общий для обеих + платформ; включает `locales.json`, `icon.png`. Старый `phymlgui.spec` из + `.gitignore` удаляется из списка игнорируемых и заменяется этим. +- Версия: тег `vX.Y.Z` → `X.Y.Z` записывается в `phyml_core/__init__.py` + `__version__` во время сборки (файл в репозитории не меняется). + +Windows — `phymlGUI-X.Y.Z-windows-x64.zip`: + +``` +phymlGUI/ phymlgui.exe _internal/ bin/{phyml-mpi.exe, iqtree2.exe, libiomp5md.dll, muscle.exe, raxml-ng.exe} licenses/ README.txt +``` + +Linux — `phymlGUI-X.Y.Z-x86_64.AppImage` (собирается `appimagetool` на +ubuntu-22.04, glibc 2.35): + +``` +AppDir/ AppRun phymlgui.desktop phymlgui.png usr/bin/{phymlgui, _internal/, bin/{phyml, phyml-mpi, iqtree2, muscle, raxml-ng}} usr/share/licenses/phymlgui/ +``` + +Без аргументов — окно; `--file`/`--batch` — консольный режим (тот же файл +работает на серверах). + +## 5. Workflow `release.yml` + +Триггеры: `push` тега `v*` и `workflow_dispatch` (пробная сборка без +публикации). Задания: + +1. `tests` — переиспользует `tests.yml` (Windows и Linux). +2. `tools-windows` — RAxML-NG через MSYS2 + загрузки → артефакт `bin-windows`. +3. `tools-linux` — PhyML/PhyML-MPI из исходников (`libopenmpi-dev`) + загрузки → + артефакт `bin-linux`. +4. `raxml-parity` — сверка Windows-сборки RAxML-NG с официальной Linux-сборкой + (деревья, бутстреп-реплики, lnL на фиксированном дереве) на тестовых данных + из `tests/data`; расхождение роняет workflow. +5. `package-windows`, `package-linux` — PyInstaller → zip / AppImage → дымовой + тест готового пакета: `--version`; CLI `--file` на невыровненном FASTA через + встроенные MUSCLE и PhyML (последовательный), отдельно IQ-TREE; проверяется, + что дерево создано; на Linux — ещё MPI-прогон через OpenMPI раннера и запуск + AppImage в контейнере `ubuntu:22.04` без дополнительных пакетов (кроме FUSE + или `--appimage-extract-and-run`); окно открывается offscreen и + закрывается. +6. `release` — только по тегу: **черновик** релиза с zip, AppImage, + `SHA256SUMS`, `raxml-ng-2.0.3-source+patch.tar.gz` и описанием с версиями + вложенных инструментов. + +Защита: тег не формата `vX.Y.Z[-suffix]` роняет workflow до сборки; ручной +запуск ничего не публикует; сборки инструментов кэшируются по SHA256 из +манифеста. + +## 6. Вне рамок + +Движок RAxML-NG в приложении («Наука»); подпись файлов, MSI, `.deb`, Flatpak; +проверка обновлений; macOS и ARM. + +## 7. Критерии готовности + +1. Полный набор тестов проходит в CI на Windows и на ubuntu-22.04. +2. `workflow_dispatch` собирает оба пакета; все дымовые тесты, MPI на Linux и + сверка RAxML-NG зелёные. +3. Тестовый тег (`v2.0.0-rc1`) создаёт черновик релиза со всеми файлами и + `SHA256SUMS`; публикация только вручную. +4. AppImage работает на чистой Ubuntu 22.04 (контейнер в CI): окно открывается, + последовательный PhyML выполняется. +5. В обоих пакетах есть `licenses/`; исходники RAxML-NG с patch'ем приложены к + релизу. + +## 8. Риски + +- Сборка PhyML из исходников под Linux может потребовать правок — это первая + задача плана; при неудаче — сообщить пользователю. +- AppImage около 200–300 МБ из-за Qt — приемлемо. +- Происхождение `phyml-mpi.exe` v1.2 известно только как «ваш прошлый релиз»; + закрепляем его хешем, пересборка под Windows вне рамок. diff --git a/locales.json b/locales.json index e47e02a..cc76f87 100644 --- a/locales.json +++ b/locales.json @@ -108,10 +108,8 @@ "tab_stats": "Stats", "tab_summary": "Summary", "panel_recent": "Recent", - "panel_presets": "Presets (v1.4)", - "panel_history": "History (v1.4)", - "panel_hint_presets": "Presets arrive in v1.4", - "panel_hint_history": "Run history arrives in v1.4", + "panel_presets": "Presets", + "panel_history": "History", "btn_settings": "Settings", "settings_title": "Settings", "settings_paths": "Executables", @@ -303,10 +301,8 @@ "tab_stats": "Статистика", "tab_summary": "Сводка", "panel_recent": "Недавние", - "panel_presets": "Пресеты (v1.4)", - "panel_history": "История (v1.4)", - "panel_hint_presets": "Пресеты появятся в v1.4", - "panel_hint_history": "История запусков появится в v1.4", + "panel_presets": "Пресеты", + "panel_history": "История", "btn_settings": "Настройки", "settings_title": "Настройки", "settings_paths": "Исполняемые файлы", @@ -329,7 +325,7 @@ "tree_tool_label": "Инструмент:", "tree_layout": "Раскладка:", "tree_layout_rect": "Прямоугольная", - "tree_layout_slant": "Сланцевая", + "tree_layout_slant": "Наклонная", "tree_layout_circ": "Круговая", "tree_supports": "Поддержки", "tree_threshold": "порог: {}%", diff --git a/packaging/appimage/AppRun b/packaging/appimage/AppRun new file mode 100644 index 0000000..e6dcdcb --- /dev/null +++ b/packaging/appimage/AppRun @@ -0,0 +1,4 @@ +#!/bin/sh +# AppImage entry point: run the bundled phymlGUI (GUI without arguments, CLI with them) +HERE="$(dirname "$(readlink -f "$0")")" +exec "$HERE/usr/bin/phymlgui" "$@" diff --git a/packaging/appimage/phymlgui.desktop b/packaging/appimage/phymlgui.desktop new file mode 100644 index 0000000..3863eeb --- /dev/null +++ b/packaging/appimage/phymlgui.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Type=Application +Name=phymlGUI +Comment=Maximum-likelihood phylogenetics with PhyML, IQ-TREE and RAxML-NG +Exec=phymlgui +Icon=phymlgui +Categories=Science;Biology; +Terminal=false diff --git a/packaging/build_phyml_linux.sh b/packaging/build_phyml_linux.sh new file mode 100644 index 0000000..1e69594 --- /dev/null +++ b/packaging/build_phyml_linux.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Build PhyML (serial) and PhyML-MPI (OpenMPI) from a source tarball. +# Usage: build_phyml_linux.sh +# Needs: build-essential autoconf automake libtool pkg-config libopenmpi-dev +set -euo pipefail + +src_archive=$(readlink -f "$1") +dest=$(mkdir -p "$2" && readlink -f "$2") +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +tar -xzf "$src_archive" -C "$work" +cd "$work"/phyml-* + +# configure.ac compiles with -march=native: a binary built on the CI runner dies with +# SIGILL on an older CPU. x86-64-v2 (SSE4.2, every x86-64 CPU since about 2009) keeps +# PhyML's SSE likelihood code; --disable-native would drop vectorisation altogether. +sed -i 's/-march=native/-march=x86-64-v2/; s/-mcpu=native/-march=x86-64-v2/' configure.ac + +# the release tarball ships a stale configured tree: regenerate from scratch +make distclean >/dev/null 2>&1 || true +sh ./autogen.sh + +no_native() { + if grep -rq -e '-march=native' -e '-mcpu=native' --include=Makefile .; then + echo 'build_phyml_linux.sh: -march=native is still in the Makefiles' >&2 + exit 1 + fi +} + +# Single-threaded make: parallel builds crashed GCC (internal compiler error +# in evolve.c) on Ubuntu 26.04; -j1 is reliable everywhere. +./configure --enable-phyml +no_native +make -j1 +install -m 755 src/phyml "$dest/phyml" + +make distclean >/dev/null +./configure --enable-phyml-mpi +no_native +make -j1 +install -m 755 src/phyml-mpi "$dest/phyml-mpi" + +echo "built: $dest/phyml $dest/phyml-mpi" diff --git a/packaging/build_raxml_windows.sh b/packaging/build_raxml_windows.sh new file mode 100644 index 0000000..da20104 --- /dev/null +++ b/packaging/build_raxml_windows.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Build a native, self-contained raxml-ng.exe (Windows x64) from the official +# source zip with the patch from docs/superpowers/spikes/2026-10-24-raxml-ng-windows. +# Run inside MSYS2 UCRT64 (mingw-w64-ucrt-x86_64-{gcc,cmake,ninja}). +# Usage: build_raxml_windows.sh +set -euo pipefail +[ -d /ucrt64/bin ] && export PATH="/ucrt64/bin:/usr/bin:$PATH" + +here=$(cd "$(dirname "$0")" && pwd) +src_archive=$(cygpath -u "$1" 2>/dev/null || echo "$1") +dest=$(cygpath -u "$2" 2>/dev/null || echo "$2") +mkdir -p "$dest" +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +unzip -q "$src_archive" -d "$work/src" +cd "$work/src" +git apply -p1 --whitespace=nowarn "$here/patches/raxml-ng-2.0.3-windows.patch" + +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DSTATIC_BUILD=ON -DPORTABLE_BUILD=ON -DCORAX_BUILD_PORTABLE_ARCH=x86-64 \ + -DUSE_MPI=OFF "-DCMAKE_CXX_FLAGS=-D_USE_MATH_DEFINES -D_GNU_SOURCE" +cmake --build build -j + +install -m 755 build/bin/raxml-ng.exe "$dest/raxml-ng.exe" +echo "built: $dest/raxml-ng.exe" diff --git a/packaging/licenses/IQ-TREE-LICENSE.txt b/packaging/licenses/IQ-TREE-LICENSE.txt new file mode 100644 index 0000000..8cdb845 --- /dev/null +++ b/packaging/licenses/IQ-TREE-LICENSE.txt @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. + diff --git a/packaging/licenses/MUSCLE-LICENSE.txt b/packaging/licenses/MUSCLE-LICENSE.txt new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/packaging/licenses/MUSCLE-LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/packaging/licenses/PhyML-COPYING.txt b/packaging/licenses/PhyML-COPYING.txt new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/packaging/licenses/PhyML-COPYING.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/packaging/licenses/RAxML-NG-LICENSE.txt b/packaging/licenses/RAxML-NG-LICENSE.txt new file mode 100644 index 0000000..dba13ed --- /dev/null +++ b/packaging/licenses/RAxML-NG-LICENSE.txt @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packaging/licenses/README.md b/packaging/licenses/README.md new file mode 100644 index 0000000..9eed6bf --- /dev/null +++ b/packaging/licenses/README.md @@ -0,0 +1,17 @@ +# Third-party licenses + +The release packages bundle these programs unchanged, except RAxML-NG on +Windows, which is built from the official sources with +`packaging/patches/raxml-ng-2.0.3-windows.patch`. + +| Program | License | File | +|---|---|---| +| PhyML | GPL-3.0 | `PhyML-COPYING.txt` | +| IQ-TREE 2 | GPL-2.0 | `IQ-TREE-LICENSE.txt` | +| MUSCLE 5 | GPL-3.0 | `MUSCLE-LICENSE.txt` | +| RAxML-NG | AGPL-3.0 | `RAxML-NG-LICENSE.txt` | + +RAxML-NG is AGPL-3.0: every release attaches +`raxml-ng-2.0.3-source+patch.tar.gz` with the exact sources and the patch +used for the Windows build. Versions and download URLs are listed in +`packaging/third_party.json`. diff --git a/packaging/package.py b/packaging/package.py new file mode 100644 index 0000000..797e663 --- /dev/null +++ b/packaging/package.py @@ -0,0 +1,155 @@ +"""Build phymlGUI release packages: a Windows zip and a Linux AppDir/AppImage. + + python packaging/package.py windows --version 2.0.0 --bin bin --out dist + python packaging/package.py linux --version 2.0.0 --bin bin --out dist --appimagetool appimagetool + +The application sources are copied to a temporary folder, __version__ is stamped +there (the repository is never modified), PyInstaller builds an onedir bundle, and +the analysis programs from --bin plus packaging/licenses/ are added. +""" +import argparse +import os +import re +import shutil +import subprocess +import sys +import tempfile +import zipfile + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +APP_SOURCES = ('phymlgui.py', 'phyml_core', 'phyml_ui', 'locales.json', 'icon.png', 'icon.ico') +TAG_RE = re.compile(r'v(\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.]*)?)') + +WINDOWS_README = """phymlGUI {version} + +Start phymlgui.exe. Command line: phymlgui-cli.exe --file data.fasta +(or --batch folder, --version). + +The analysis programs are in bin/; their licenses are in licenses/. +PhyML needs Microsoft MPI (msmpisetup.exe from Microsoft), also for serial runs: +the bundled PhyML is an MPI build. +Settings are kept next to phymlgui.exe, or in %APPDATA%\\phymlGUI if this +folder is read-only. +""" + + +def version_from_tag(tag): + """'v2.0.0' -> '2.0.0', 'v2.0.0-rc1' -> '2.0.0-rc1'; anything else -> ValueError.""" + m = TAG_RE.fullmatch(tag) + if not m: + raise ValueError(f'not a release tag (vX.Y.Z[-suffix]): {tag!r}') + return m.group(1) + + +def stamp_version(version, init_path): + """Rewrite __version__ in phyml_core/__init__.py at init_path (a build copy).""" + with open(init_path, encoding='utf-8') as f: + text = f.read() + text, n = re.subn(r'__version__\s*=\s*["\'][^"\']*["\']', f'__version__ = "{version}"', text) + if n != 1: + raise ValueError(f'__version__ not found in {init_path}') + with open(init_path, 'w', encoding='utf-8', newline='') as f: + f.write(text) + + +def _pyinstaller_bundle(version, work): + """Copy the sources into work/src, stamp the version, run PyInstaller -> bundle dir.""" + src = os.path.join(work, 'src') + os.makedirs(src) + for name in APP_SOURCES: + path = os.path.join(ROOT, name) + if os.path.isdir(path): + shutil.copytree(path, os.path.join(src, name), + ignore=shutil.ignore_patterns('__pycache__')) + elif os.path.isfile(path): + shutil.copy2(path, src) + os.makedirs(os.path.join(src, 'packaging')) + shutil.copy2(os.path.join(ROOT, 'packaging', 'phymlgui.spec'), os.path.join(src, 'packaging')) + stamp_version(version, os.path.join(src, 'phyml_core', '__init__.py')) + subprocess.run([sys.executable, '-m', 'PyInstaller', '--noconfirm', '--clean', + '--distpath', os.path.join(work, 'dist'), + '--workpath', os.path.join(work, 'build'), + os.path.join(src, 'packaging', 'phymlgui.spec')], check=True) + return os.path.join(work, 'dist', 'phymlgui') + + +def _copy_tools(bin_dir, target): + os.makedirs(target, exist_ok=True) + for name in sorted(os.listdir(bin_dir)): + shutil.copy2(os.path.join(bin_dir, name), os.path.join(target, name)) + + +def build_windows(version, bin_dir, out_dir): + """phymlGUI--windows-x64.zip with phymlGUI/{exe, _internal, bin, licenses}.""" + os.makedirs(out_dir, exist_ok=True) + with tempfile.TemporaryDirectory(prefix='phymlgui_pkg_') as work: + bundle = _pyinstaller_bundle(version, work) + app = os.path.join(work, 'phymlGUI') + shutil.move(bundle, app) + _copy_tools(bin_dir, os.path.join(app, 'bin')) + shutil.copytree(os.path.join(ROOT, 'packaging', 'licenses'), os.path.join(app, 'licenses')) + with open(os.path.join(app, 'README.txt'), 'w', encoding='utf-8', newline='\r\n') as f: + f.write(WINDOWS_README.format(version=version)) + out = os.path.join(out_dir, f'phymlGUI-{version}-windows-x64.zip') + with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as z: + for folder, _, files in os.walk(app): + for name in files: + path = os.path.join(folder, name) + z.write(path, os.path.relpath(path, work)) + return out + + +def build_appdir(version, bin_dir, out_dir): + """An AppDir in out_dir: AppRun, .desktop, icon, usr/bin/{phymlgui, _internal, bin/}.""" + appdir = os.path.join(out_dir, 'phymlGUI.AppDir') + shutil.rmtree(appdir, ignore_errors=True) + os.makedirs(out_dir, exist_ok=True) + with tempfile.TemporaryDirectory(prefix='phymlgui_pkg_') as work: + bundle = _pyinstaller_bundle(version, work) + usr_bin = os.path.join(appdir, 'usr', 'bin') + shutil.copytree(bundle, usr_bin) + _copy_tools(bin_dir, os.path.join(usr_bin, 'bin')) + for name in os.listdir(os.path.join(usr_bin, 'bin')): + os.chmod(os.path.join(usr_bin, 'bin', name), 0o755) + shutil.copytree(os.path.join(ROOT, 'packaging', 'licenses'), + os.path.join(appdir, 'usr', 'share', 'licenses', 'phymlgui')) + apprun = os.path.join(appdir, 'AppRun') + shutil.copy2(os.path.join(ROOT, 'packaging', 'appimage', 'AppRun'), apprun) + os.chmod(apprun, 0o755) + shutil.copy2(os.path.join(ROOT, 'packaging', 'appimage', 'phymlgui.desktop'), appdir) + shutil.copy2(os.path.join(ROOT, 'icon.png'), os.path.join(appdir, 'phymlgui.png')) + return appdir + + +def build_appimage(version, bin_dir, out_dir, appimagetool, runtime=None): + """phymlGUI--x86_64.AppImage built by the pinned appimagetool (and runtime).""" + if not appimagetool: + raise ValueError('--appimagetool is required for linux') + appdir = build_appdir(version, bin_dir, out_dir) + out = os.path.join(out_dir, f'phymlGUI-{version}-x86_64.AppImage') + command = [appimagetool, '--appimage-extract-and-run'] # no FUSE needed on CI + if runtime: + command += ['--runtime-file', runtime] + subprocess.run([*command, appdir, out], check=True, env=dict(os.environ, ARCH='x86_64')) + return out + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('platform', choices=['windows', 'linux']) + parser.add_argument('--version', required=True) + parser.add_argument('--bin', required=True, help='folder with the analysis programs') + parser.add_argument('--out', default='dist') + parser.add_argument('--appimagetool', help='path to appimagetool (linux)') + parser.add_argument('--runtime', help='path to the pinned AppImage runtime (linux)') + args = parser.parse_args(argv) + if args.platform == 'windows': + out = build_windows(args.version, args.bin, args.out) + else: + out = build_appimage(args.version, args.bin, args.out, args.appimagetool, args.runtime) + print(out) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/packaging/patches/raxml-ng-2.0.3-windows.patch b/packaging/patches/raxml-ng-2.0.3-windows.patch new file mode 100644 index 0000000..8d64993 --- /dev/null +++ b/packaging/patches/raxml-ng-2.0.3-windows.patch @@ -0,0 +1,470 @@ +Binary files pristine/bin/raxml-ng.exe and dl_src/bin/raxml-ng.exe differ +diff -ruN '--exclude=src.zip' pristine/libs/coraxlib/src/corax/core/common.h dl_src/libs/coraxlib/src/corax/core/common.h +--- pristine/libs/coraxlib/src/corax/core/common.h 2025-10-31 18:41:19.000000000 +0300 ++++ dl_src/libs/coraxlib/src/corax/core/common.h 2026-10-24 19:21:29.744785700 +0300 +@@ -44,7 +44,7 @@ + #include + #endif + +-#if (defined(__WIN32__) || defined(__WIN64__)) ++#if (defined(__WIN32__) || defined(__WIN64__)) && defined(CORAX_SHARED) + #define CORAX_EXPORT __declspec(dllexport) + #else + #define CORAX_EXPORT +diff -ruN '--exclude=src.zip' pristine/libs/coraxlib/src/corax/core/partition.c dl_src/libs/coraxlib/src/corax/core/partition.c +--- pristine/libs/coraxlib/src/corax/core/partition.c 2025-10-31 18:41:19.000000000 +0300 ++++ dl_src/libs/coraxlib/src/corax/core/partition.c 2026-10-24 22:38:25.828525000 +0300 +@@ -45,7 +45,7 @@ + + if (partition->tipchars) + for (i = 0; i < partition->tips; ++i) +- corax_aligned_free(partition->tipchars[i]); ++ free(partition->tipchars[i]); + free(partition->tipchars); + + if (partition->ttlookup) corax_aligned_free(partition->ttlookup); +@@ -114,7 +114,7 @@ + free(repeats->lookup_buffer); + free(repeats->toclean_buffer); + free(repeats->id_site_buffer); +- free(repeats->bclv_buffer); ++ corax_aligned_free(repeats->bclv_buffer); + free(repeats->charmap); + free(repeats); + } +@@ -236,7 +236,7 @@ + && CORAX_HAS_CPU_FEATURE(avx_present)) + return CORAX_SUCCESS; + +- free(partition->ttlookup); ++ corax_aligned_free(partition->ttlookup); + partition->ttlookup = + corax_aligned_alloc(alloc_size * sizeof(double), partition->alignment); + if (!partition->ttlookup) +diff -ruN '--exclude=src.zip' pristine/libs/coraxlib/src/corax/core/repeats.c dl_src/libs/coraxlib/src/corax/core/repeats.c +--- pristine/libs/coraxlib/src/corax/core/repeats.c 2025-10-31 18:41:19.000000000 +0300 ++++ dl_src/libs/coraxlib/src/corax/core/repeats.c 2026-10-24 22:37:42.855902200 +0300 +@@ -221,7 +221,7 @@ + } + unsigned int sizealloc = (ids + additional_sites) * partition->states_padded + * partition->rate_cats * sizeof(double); +- free(partition->clv[tip_index]); ++ corax_aligned_free(partition->clv[tip_index]); + + partition->clv[tip_index] = + corax_aligned_alloc(sizealloc, partition->alignment); +diff -ruN '--exclude=src.zip' pristine/libs/coraxlib/src/corax/io/binary.c dl_src/libs/coraxlib/src/corax/io/binary.c +--- pristine/libs/coraxlib/src/corax/io/binary.c 2026-03-11 21:17:41.000000000 +0300 ++++ dl_src/libs/coraxlib/src/corax/io/binary.c 2026-10-24 22:37:47.461549500 +0300 +@@ -1029,7 +1029,10 @@ + if (!bin_fread(data, *size, 1, bin_file)) + { + corax_set_error(CORAX_BIN_ERROR_BINARY_IO, "Error reading data."); +- free(data); ++ if (*attributes & CORAX_BIN_ATTRIB_ALIGNED) ++ corax_aligned_free(data); ++ else ++ free(data); + return CORAX_FAILURE; + } + +diff -ruN '--exclude=src.zip' pristine/libs/coraxlib/src/corax/tree/treeinfo.c dl_src/libs/coraxlib/src/corax/tree/treeinfo.c +--- pristine/libs/coraxlib/src/corax/tree/treeinfo.c 2026-04-25 00:35:45.000000000 +0300 ++++ dl_src/libs/coraxlib/src/corax/tree/treeinfo.c 2026-10-24 22:37:44.852293600 +0300 +@@ -791,7 +791,7 @@ + + if (treeinfo->deriv_precomp[partition_index]) + { +- free(treeinfo->deriv_precomp[partition_index]); ++ corax_aligned_free(treeinfo->deriv_precomp[partition_index]); + treeinfo->deriv_precomp[partition_index] = NULL; + } + +diff -ruN '--exclude=src.zip' pristine/libs/coraxlib/src/corax/util/stepwise.c dl_src/libs/coraxlib/src/corax/util/stepwise.c +--- pristine/libs/coraxlib/src/corax/util/stepwise.c 2026-03-11 21:17:41.000000000 +0300 ++++ dl_src/libs/coraxlib/src/corax/util/stepwise.c 2026-10-24 22:35:47.977481800 +0300 +@@ -85,7 +85,9 @@ + { + int rint; + corax_random_r(buf, &rint); +- double r = ((double)rint / RAND_MAX); ++ /* corax_random_r yields [0, 2^31-1] everywhere, while RAND_MAX is ++ only 32767 on Windows: normalize by the generator's own maximum */ ++ double r = ((double)rint / 2147483647.0); + j = (unsigned int)(r * (i + 1)); + + CORAX_SWAP(x[i], x[j]); +diff -ruN '--exclude=src.zip' pristine/src/adaptive/DifficultyPredictor.cpp dl_src/src/adaptive/DifficultyPredictor.cpp +--- pristine/src/adaptive/DifficultyPredictor.cpp 2026-09-03 19:23:46.000000000 +0300 ++++ dl_src/src/adaptive/DifficultyPredictor.cpp 2026-10-24 22:46:13.171567100 +0300 +@@ -76,7 +76,7 @@ + // init seeds + uintVector seeds(n_trees); + for (unsigned int i = 0; i < n_trees; ++i) +- seeds[i] = (unsigned int) rand(); ++ seeds[i] = (unsigned int) raxml_rand(); + + for (unsigned int i = 0; i < n_trees; ++i) + _pars_tree_list.emplace_back(Tree::buildParsimony(*_parsimony_msa_ptr, seeds[i], false, &score)); +diff -ruN '--exclude=src.zip' pristine/src/adaptive/StoppingCriterion.cpp dl_src/src/adaptive/StoppingCriterion.cpp +--- pristine/src/adaptive/StoppingCriterion.cpp 2026-09-03 19:23:46.000000000 +0300 ++++ dl_src/src/adaptive/StoppingCriterion.cpp 2026-10-24 22:46:13.163049200 +0300 +@@ -391,14 +391,14 @@ + + { + ParallelContext::UniqueLock lock; +- srand(seed); ++ raxml_srand(seed); + + for(exp = 0; exp < rell_size; exp++){ + + double rell_loglh = 0; + for (i = 0; i p = find_by_site_index(site_index); + assert(p.first >= 0 && p.second >= 0); + auto p_id = (unsigned int) p.first; +@@ -410,8 +410,8 @@ + } + + for(exp = 0; exp < rell_size; exp++){ +- auto ind1 = (unsigned int) rand() % rell_size; +- auto ind2 = (unsigned int) rand() % rell_size; ++ auto ind1 = (unsigned int) raxml_rand() % rell_size; ++ auto ind2 = (unsigned int) raxml_rand() % rell_size; + logl_diff_dist[exp] = fabs(rell_dist[ind1] - rell_dist[ind2]); + } + } +diff -ruN '--exclude=src.zip' pristine/src/common.h dl_src/src/common.h +--- pristine/src/common.h 2026-05-27 16:34:30.000000000 +0300 ++++ dl_src/src/common.h 2026-10-24 22:46:05.179337100 +0300 +@@ -15,6 +15,12 @@ + #include "log.hpp" + #include "util/SystemTimer.hpp" + ++/* rand()/srand() with glibc semantics on every platform: Windows' rand() is a ++ 15-bit LCG, so per-tree and per-replicate seeds drawn from it collide ++ (duplicate bootstrap replicates) and differ from Linux for the same --seed */ ++int raxml_rand(); ++void raxml_srand(unsigned int seed); ++ + /* used to suppress compiler warnings about unused args */ + #define RAXML_UNUSED(expr) (void)(expr) + +@@ -26,8 +32,8 @@ + + double sysutil_gettime(); + void sysutil_show_rusage(); +-unsigned long sysutil_get_memused(); +-unsigned long sysutil_get_memtotal(bool ignore_errors = true); ++unsigned long long sysutil_get_memused(); ++unsigned long long sysutil_get_memtotal(bool ignore_errors = true); + + std::string sysutil_get_cpu_model(); + unsigned int sysutil_get_cpu_cores(); +diff -ruN '--exclude=src.zip' pristine/src/main.cpp dl_src/src/main.cpp +--- pristine/src/main.cpp 2026-09-03 19:23:46.000000000 +0300 ++++ dl_src/src/main.cpp 2026-10-24 22:46:13.153672200 +0300 +@@ -1314,7 +1314,7 @@ + // temp workaround: since MSA pattern compression calls rand(), it will change all random + // numbers generated afterwards. so just reset seed to the initial value to ensure that + // starting trees, BS replicates etc. are the same regardless whether pat.comp is ON or OFF +- srand(opts.random_seed); ++ raxml_srand(opts.random_seed); + } + + // if (parted_msa.part_count() > 1) +@@ -1851,7 +1851,7 @@ + // init seeds + uintVector seeds(tree_count); + for (size_t i = 0; i < tree_count; ++i) +- seeds[i] = (unsigned int) rand(); ++ seeds[i] = (unsigned int) raxml_rand(); + + auto thread_fn = std::bind(thread_start_trees, + std::ref(instance), +@@ -1946,7 +1946,7 @@ + // init seeds + uintVector seeds(st_tree_count); + for (size_t i = 0; i < st_tree_count; ++i) +- seeds[i] = (unsigned int) rand(); ++ seeds[i] = (unsigned int) raxml_rand(); + + for (size_t i = 0; i < st_tree_count; ++i) + { +@@ -2178,7 +2178,7 @@ + auto& seeds = instance.bs_seeds; + seeds.resize(num_bsreps); + for (size_t i = 0; i < seeds.size(); ++i) +- seeds[i] = (unsigned int) rand(); ++ seeds[i] = (unsigned int) raxml_rand(); + + /* generate missing parsimony trees from the original MSA */ + if (instance.opts.num_pars_trees() > instance.pars_trees.size()) +@@ -2191,7 +2191,7 @@ + { + for (size_t i = 0; i < trees_to_generate; ++i) + { +- auto tree = generate_tree(instance, StartingTree::parsimony, (unsigned int) rand()); ++ auto tree = generate_tree(instance, StartingTree::parsimony, (unsigned int) raxml_rand()); + instance.pars_trees.emplace_back(std::move(tree)); + } + } +@@ -2378,7 +2378,7 @@ + /* no starting trees -> use a first tree from pars_trees, generate if needed */ + if (instance.pars_trees.empty()) + { +- auto tree = generate_tree(instance, StartingTree::parsimony, (unsigned int) rand()); ++ auto tree = generate_tree(instance, StartingTree::parsimony, (unsigned int) raxml_rand()); + instance.pars_trees.emplace_back(std::move(tree)); + } + } +@@ -2740,7 +2740,7 @@ + + if (bs_num % opts.bootstop_interval == 0 || bs_num == bs_trees.size()) + { +- converged = bootstop_checker->converged((unsigned int) rand()); ++ converged = bootstop_checker->converged((unsigned int) raxml_rand()); + + if (print) + { +@@ -3474,7 +3474,7 @@ + + // we need at least 2 doubles for each partition AND threads to perform parallel reduction, + // and even more with FreeRates in EM mode -> so resize the buffer accordingly +- auto doubles_per_thread_part = std::max(2UL, total_freerate_category_count); ++ auto doubles_per_thread_part = std::max(2, total_freerate_category_count); + const size_t reduce_buffer_size = std::max(1024u, doubles_per_thread_part * sizeof(double) * + parted_msa.part_count() * ParallelContext::num_threads()); + +@@ -4012,8 +4012,8 @@ + check_options(instance); + + /* init template tree */ +- srand(instance.opts.random_seed); +- instance.random_tree = generate_tree(instance, StartingTree::random, (unsigned int) rand()); ++ raxml_srand(instance.opts.random_seed); ++ instance.random_tree = generate_tree(instance, StartingTree::random, (unsigned int) raxml_rand()); + + // read start trees from file to avoid re-generation + // NOTE: doesn't work for OLD constrained tree search +@@ -4050,7 +4050,7 @@ + + // TEMP WORKAROUND: here we reset random seed once again to make sure that BS replicates + // are not affected by the number of ML search starting trees that has been generated before +- srand(instance.opts.random_seed); ++ raxml_srand(instance.opts.random_seed); + + /* generate bootstrap replicates */ + generate_bootstraps(instance, cm.checkp_file()); +@@ -4246,7 +4246,7 @@ + { + // make sure all MPI ranks use the same random seed + ParallelContext::mpi_broadcast(&opts.random_seed, sizeof(unsigned int)); +- srand(opts.random_seed); ++ raxml_srand(opts.random_seed); + + logger().log_level(instance.opts.log_level); + logger().precision(instance.opts.precision); +diff -ruN '--exclude=src.zip' pristine/src/util/sysutil.cpp dl_src/src/util/sysutil.cpp +--- pristine/src/util/sysutil.cpp 2026-09-03 19:23:46.000000000 +0300 ++++ dl_src/src/util/sysutil.cpp 2026-10-24 22:46:07.639509900 +0300 +@@ -1,8 +1,10 @@ +-#if (!defined(_WIN32) && !defined(__aarch64__)) ++#if ((!defined(_WIN32) || defined(__GNUC__)) && !defined(__aarch64__)) + #include + #endif + #include ++#ifndef _WIN32 + #include ++#endif + #include + #include + #if defined __APPLE__ +@@ -20,6 +22,55 @@ + + #include "../common.h" + ++#ifdef _WIN32 ++#ifndef WIN32_LEAN_AND_MEAN ++#define WIN32_LEAN_AND_MEAN ++#endif ++#ifndef NOMINMAX ++#define NOMINMAX ++#endif ++#include ++#include ++ ++static double filetime_to_sec(const FILETIME& ft) ++{ ++ return (double)(((ULONGLONG)ft.dwHighDateTime << 32) | ft.dwLowDateTime) * 1.0e-7; ++} ++ ++#include ++ ++static std::mutex rand_mutex; ++static struct corax_random_data rand_data; ++static char rand_state[128]; /* 128 bytes = glibc's default TYPE_3 state */ ++static bool rand_seeded = false; ++ ++static void rand_seed_locked(unsigned int seed) ++{ ++ memset(&rand_data, 0, sizeof(rand_data)); ++ corax_initstate_r(seed, rand_state, sizeof(rand_state), &rand_data); ++ rand_seeded = true; ++} ++ ++void raxml_srand(unsigned int seed) ++{ ++ std::lock_guard lock(rand_mutex); ++ rand_seed_locked(seed); ++} ++ ++int raxml_rand() ++{ ++ std::lock_guard lock(rand_mutex); ++ if (!rand_seeded) ++ rand_seed_locked(1); /* as in glibc: rand() before srand() == srand(1) */ ++ int32_t r; ++ corax_random_r(&rand_data, &r); ++ return r; ++} ++#else ++int raxml_rand() { return rand(); } ++void raxml_srand(unsigned int seed) { srand(seed); } ++#endif ++ + using namespace std; + + SystemTimer systimer; +@@ -55,8 +106,13 @@ + + void * xmemalign(size_t size, size_t alignment) + { ++#ifdef _WIN32 ++ void * t = _aligned_malloc(size, alignment); ++ int err = 0; ++#else + void * t = nullptr; + int err = posix_memalign(& t, alignment, size); ++#endif + + if (err || !t) + sysutil_fatal("Unable to allocate enough memory."); +@@ -85,11 +141,18 @@ + + void sysutil_show_rusage() + { ++#ifdef _WIN32 ++ FILETIME creation, exit_time, kernel, user; ++ GetProcessTimes(GetCurrentProcess(), &creation, &exit_time, &kernel, &user); ++ fprintf(stderr, "Time: %.3fs (user)", filetime_to_sec(user)); ++ fprintf(stderr, " %.3fs (sys)", filetime_to_sec(kernel)); ++ fprintf(stderr, " Memory: %.0fMB\n", sysutil_get_memused() * 1.0e-6); ++#else + struct rusage r_usage; + getrusage(RUSAGE_SELF, & r_usage); +- ++ + fprintf(stderr, +- "Time: %.3fs (user)", ++ "Time: %.3fs (user)", + r_usage.ru_utime.tv_sec * 1.0 + (double)r_usage.ru_utime.tv_usec * 1.0e-6); + fprintf(stderr, + " %.3fs (sys)", +@@ -102,10 +165,17 @@ + /* Linux: ru_maxrss gives the size in kilobytes */ + fprintf(stderr, " Memory: %.0fMB\n", r_usage.ru_maxrss * 1.0e-3); + #endif ++#endif + } + +-unsigned long sysutil_get_memused() ++unsigned long long sysutil_get_memused() + { ++#ifdef _WIN32 ++ PROCESS_MEMORY_COUNTERS pmc; ++ if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) ++ return (unsigned long long) pmc.PeakWorkingSetSize; ++ return 0; ++#else + struct rusage r_usage; + getrusage(RUSAGE_SELF, & r_usage); + +@@ -116,11 +186,25 @@ + /* Linux: ru_maxrss gives the size in kilobytes */ + return (unsigned long)r_usage.ru_maxrss * 1024; + #endif ++#endif + } + +-unsigned long sysutil_get_memtotal(bool ignore_errors) ++unsigned long long sysutil_get_memtotal(bool ignore_errors) + { +-#if defined(_SC_PHYS_PAGES) && defined(_SC_PAGESIZE) ++#if defined(_WIN32) ++ ++ MEMORYSTATUSEX status; ++ status.dwLength = sizeof(status); ++ if (!GlobalMemoryStatusEx(&status)) ++ { ++ if (ignore_errors) ++ return 0; ++ else ++ throw runtime_error("Cannot determine amount of RAM"); ++ } ++ return status.ullTotalPhys; ++ ++#elif defined(_SC_PHYS_PAGES) && defined(_SC_PAGESIZE) + + long phys_pages = sysconf(_SC_PHYS_PAGES); + long pagesize = sysconf(_SC_PAGESIZE); +@@ -178,7 +262,7 @@ + // not supported + RAXML_UNUSED(out); + RAXML_UNUSED(x); +-#elif defined(_WIN32) ++#elif defined(_WIN32) && !defined(__GNUC__) + __cpuid(out, x); + #else + __cpuid_count(x, 0, out[0], out[1], out[2], out[3]); +@@ -295,7 +379,11 @@ + + long sysutil_online_cpu_cores() + { ++#ifdef _WIN32 ++ return (long) std::thread::hardware_concurrency(); ++#else + return sysconf(_SC_NPROCESSORS_ONLN); ++#endif + } + + unsigned long sysutil_get_cpu_features() +@@ -314,7 +402,7 @@ + int32_t nIds = info[0]; + + get_cpuid(info, 0x80000000); +- u_int32_t nExIds = info[0]; ++ uint32_t nExIds = info[0]; + + // Detect Features + if (nIds >= 0x00000001) +@@ -463,7 +551,16 @@ + + std::string sysutil_realpath(const std::string& path) + { ++#ifdef _WIN32 ++ // _fullpath does not check existence: mimic realpath's ENOENT ++ char * real_path = nullptr; ++ if (sysutil_file_exists(path) || sysutil_dir_exists(path)) ++ real_path = _fullpath(nullptr, path.c_str(), 0); ++ else ++ errno = ENOENT; ++#else + char * real_path = realpath(path.c_str(), nullptr); ++#endif + if (real_path) + { + const string result(real_path); diff --git a/packaging/phymlgui.spec b/packaging/phymlgui.spec new file mode 100644 index 0000000..3d1144d --- /dev/null +++ b/packaging/phymlgui.spec @@ -0,0 +1,31 @@ +# -*- mode: python ; coding: utf-8 -*- +# PyInstaller spec for phymlGUI release packages (onedir), run by packaging/package.py +# from a temporary copy of the sources (SPECPATH = /packaging). +# Windows gets two executables sharing one _internal/: phymlgui.exe (windowed GUI) +# and phymlgui-cli.exe (console, for --file/--batch/--version). Linux gets one. +import os +import sys + +src = os.path.dirname(SPECPATH) +windows = sys.platform == 'win32' +icon = os.path.join(src, 'icon.ico') if windows else None + +a = Analysis( + [os.path.join(src, 'phymlgui.py')], + pathex=[src], + datas=[(os.path.join(src, 'locales.json'), '.'), (os.path.join(src, 'icon.png'), '.')], + hiddenimports=['openpyxl'], + excludes=['tkinter', 'pytest'], + noarchive=False, +) +pyz = PYZ(a.pure) + +gui = EXE(pyz, a.scripts, [], exclude_binaries=True, name='phymlgui', + console=not windows, icon=icon, upx=False) +targets = [gui] +if windows: + cli = EXE(pyz, a.scripts, [], exclude_binaries=True, name='phymlgui-cli', + console=True, icon=icon, upx=False) + targets.append(cli) + +coll = COLLECT(*targets, a.binaries, a.datas, strip=False, upx=False, name='phymlgui') diff --git a/packaging/raxml_parity.py b/packaging/raxml_parity.py new file mode 100644 index 0000000..d8070d8 --- /dev/null +++ b/packaging/raxml_parity.py @@ -0,0 +1,144 @@ +"""Check that our Windows RAxML-NG build matches the official Linux binary. + +The two binaries run on different machines in CI, so this has two steps: + + python packaging/raxml_parity.py run --exe raxml-ng.exe --out parity-windows + python packaging/raxml_parity.py run --exe ./raxml-ng --out parity-linux + python packaging/raxml_parity.py compare parity-windows parity-linux + +`run` builds a fixed synthetic alignment and runs, with --seed 42: an ML search, +50 bootstrap replicates, and a likelihood evaluation of a fixed tree under JC. +`compare` applies the rules from the RAxML-NG Windows spike: identical best tree +(RF 0) with |ΔlnL| < 1e-3, identical bootstrap replicates, and the same +fixed-tree likelihood to 6 decimals. +""" +import argparse +import io +import os +import re +import subprocess +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +LNL_TOLERANCE = 1e-3 +FIXED_TREE = ('(((A1_testsp:0.02,A2_testsp:0.02):0.02,(A3_testsp:0.02,A4_testsp:0.02):0.02):0.15,' + '((B1_testsp:0.02,B2_testsp:0.02):0.02,(B3_testsp:0.02,B4_testsp:0.02):0.02):0.15);') + + +def _read(folder, name): + with open(os.path.join(folder, name), encoding='utf-8', errors='replace') as f: + return f.read().replace('\r\n', '\n') + + +def _lnl(text): + m = re.search(r'Final LogLikelihood:\s*(-?[\d.]+)', text) + if not m: + raise ValueError('no "Final LogLikelihood" line') + return float(m.group(1)) + + +def _splits(newick): + from Bio import Phylo + tree = Phylo.read(io.StringIO(newick), 'newick') + names = {t.name for t in tree.get_terminals()} + anchor = min(names) + out = set() + for clade in tree.get_nonterminals(): + side = frozenset(t.name for t in clade.get_terminals()) + side = side if anchor not in side else frozenset(names - side) + if 1 < len(side) < len(names) - 1: + out.add(side) + return out + + +def compare(win_dir, lin_dir): + """Differences between the two output folders; an empty list means they match.""" + problems = [] + + def check(name, test): + try: + message = test(_read(win_dir, name), _read(lin_dir, name)) + except (OSError, ValueError) as e: + message = str(e) + if message: + problems.append(f'{name}: {message}') + + def best_tree(w, l): + if _splits(w.strip()) != _splits(l.strip()): + return 'best trees differ (RF > 0)' + + def search_lnl(w, l): + diff = abs(_lnl(w) - _lnl(l)) + if diff >= LNL_TOLERANCE: + return f'|ΔlnL| = {diff:.2e} >= {LNL_TOLERANCE:g}' + + def bootstraps(w, l): + win_trees = [t for t in w.splitlines() if t.strip()] + lin_trees = [t for t in l.splitlines() if t.strip()] + if len(win_trees) != len(lin_trees): + return f'{len(win_trees)} vs {len(lin_trees)} replicates' + differing = sum(_splits(a) != _splits(b) for a, b in zip(win_trees, lin_trees)) + if differing: + return f'{differing} of {len(win_trees)} replicate trees differ' + + def fixed(w, l): + if round(_lnl(w), 6) != round(_lnl(l), 6): + return f'fixed-tree lnL {_lnl(w)} vs {_lnl(l)}' + + check('search.raxml.bestTree', best_tree) + check('search.raxml.log', search_lnl) + check('boot.raxml.bootstraps', bootstraps) + check('eval.raxml.log', fixed) + return problems + + +def run(exe, out): + """Run the parity analyses with exe into out (inputs are generated here).""" + sys.path.insert(0, os.path.join(ROOT, 'tests', 'e2e')) + sys.path.insert(0, ROOT) + from dataset_gen import make_dataset, write_fasta + from phyml_core import alignment + os.makedirs(out, exist_ok=True) + names, aligned, _ = make_dataset() + fasta = os.path.join(out, 'parity.fasta') + write_fasta(fasta, names, aligned) + phy = os.path.join(out, 'parity.phy') + alignment.write_phylip(alignment.read_fasta(fasta), phy) + with open(os.path.join(out, 'fixed.tree'), 'w') as f: + f.write(FIXED_TREE + '\n') + # the same SIMD kernel on both runners: RAxML-NG otherwise picks one per CPU + common = ['--msa', 'parity.phy', '--seed', '42', '--redo', '--simd', 'avx2'] + runs = [ + ['--search', *common, '--model', 'GTR+G', '--threads', '1', '--prefix', 'search'], + ['--bootstrap', *common, '--model', 'GTR+G', '--threads', '2', '--bs-trees', '50', + '--prefix', 'boot'], + ['--evaluate', *common, '--model', 'JC', '--tree', 'fixed.tree', '--opt-model', 'off', + '--opt-branches', 'off', '--threads', '1', '--prefix', 'eval'], + ] + for args in runs: + subprocess.run([os.path.abspath(exe), *args], cwd=out, check=True, + stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest='command', required=True) + r = sub.add_parser('run') + r.add_argument('--exe', required=True) + r.add_argument('--out', required=True) + c = sub.add_parser('compare') + c.add_argument('win_dir') + c.add_argument('lin_dir') + args = parser.parse_args(argv) + if args.command == 'run': + run(args.exe, args.out) + return 0 + problems = compare(args.win_dir, args.lin_dir) + for p in problems: + print('DIFF', p) + print('RAxML-NG parity: ' + ('OK' if not problems else f'{len(problems)} difference(s)')) + return 1 if problems else 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/packaging/release_meta.py b/packaging/release_meta.py new file mode 100644 index 0000000..b4a942d --- /dev/null +++ b/packaging/release_meta.py @@ -0,0 +1,141 @@ +"""Release metadata for the GitHub workflow: version, SHA256SUMS, notes, AGPL sources. + + python packaging/release_meta.py version "$GITHUB_REF" + python packaging/release_meta.py sha256sums out/SHA256SUMS out/*.zip out/*.AppImage + python packaging/release_meta.py notes 2.0.0 out/notes.md + python packaging/release_meta.py raxml-source +""" +import argparse +import hashlib +import importlib.util +import json +import os +import sys +import tarfile + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MANIFEST = os.path.join(ROOT, 'packaging', 'third_party.json') +DEV_VERSION = '0.0.0-dev' + +_spec = importlib.util.spec_from_file_location('package', os.path.join(ROOT, 'packaging', 'package.py')) +_package = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_package) + +NOTES_DIR = os.path.join(ROOT, 'packaging', 'release_notes') +TOOL_NAMES = {'phyml': 'PhyML', 'iqtree': 'IQ-TREE', 'muscle': 'MUSCLE', 'raxml': 'RAxML-NG'} + +DOWNLOAD_EN = """### Download and requirements + +- **Windows 10/11, 64-bit:** `phymlGUI-{version}-windows-x64.zip`. Unpack the whole archive and run `phymlgui.exe` (`phymlgui-cli.exe` for the command line). PhyML needs [Microsoft MPI](https://github.com/microsoft/Microsoft-MPI/releases) (`msmpisetup.exe`), also for serial runs: the bundled PhyML is an MPI build. +- **Linux x86_64:** `phymlGUI-{version}-x86_64.AppImage`. Run `chmod +x` and start it; without FUSE add `--appimage-extract-and-run`. Needs glibc 2.35 or newer (Ubuntu 22.04+). MPI mode needs OpenMPI: `sudo apt install openmpi-bin`. +- `SHA256SUMS`: checksums of the files, `sha256sum -c SHA256SUMS`. +- `raxml-ng-2.0.3-source+patch.tar.gz`: RAxML-NG sources and the patch of the Windows build (AGPL-3.0). + +Python is not required. + +### Bundled programs + +| Program | Windows | Linux | +| --- | --- | --- | +{tools} +""" + +DOWNLOAD_RU = """### Скачать и требования + +- **Windows 10/11, 64 бит:** `phymlGUI-{version}-windows-x64.zip`. Распакуйте архив целиком и запустите `phymlgui.exe` (`phymlgui-cli.exe` — для командной строки). PhyML требует [Microsoft MPI](https://github.com/microsoft/Microsoft-MPI/releases) (`msmpisetup.exe`), в том числе для последовательного режима: встроенный PhyML собран с MPI. +- **Linux x86_64:** `phymlGUI-{version}-x86_64.AppImage`. Выполните `chmod +x` и запустите; без FUSE добавьте `--appimage-extract-and-run`. Нужен glibc 2.35 или новее (Ubuntu 22.04+). Для MPI-режима нужен OpenMPI: `sudo apt install openmpi-bin`. +- `SHA256SUMS` — контрольные суммы файлов, `sha256sum -c SHA256SUMS`. +- `raxml-ng-2.0.3-source+patch.tar.gz` — исходники RAxML-NG и патч сборки для Windows (AGPL-3.0). + +Python не нужен. + +### Программы в комплекте + +| Программа | Windows | Linux | +| --- | --- | --- | +{tools} +""" + + +def version_for_ref(ref): + """'refs/tags/vX.Y.Z[-s]' -> 'X.Y.Z[-s]' (ValueError for any other tag); + a branch ref (manual run) -> '0.0.0-dev'.""" + if ref.startswith('refs/tags/'): + return _package.version_from_tag(ref[len('refs/tags/'):]) + return DEV_VERSION + + +def write_sha256sums(files, out): + with open(out, 'w', encoding='utf-8', newline='\n') as f: + for path in files: + with open(path, 'rb') as src: + digest = hashlib.sha256(src.read()).hexdigest() + f.write(f'{digest} {os.path.basename(path)}\n') + + +def _highlights(version, suffix, notes_dir): + """packaging/release_notes/; a pre-release uses its release's file.""" + path = os.path.join(notes_dir, version.split('-')[0] + suffix) + if not os.path.isfile(path): + return '' + with open(path, encoding='utf-8') as f: + return f.read().strip() + '\n\n' + + +def release_notes(version, manifest, notes_dir=NOTES_DIR): + """English, then Russian: the version's highlights, downloads and bundled programs.""" + rows = [] + for name, label in TOOL_NAMES.items(): + # '2.0.3 (native build, ...)' -> '2.0.3': the build notes are in the text above + cells = [manifest[section].get(name, {}).get('version', '—').split(' (')[0] + for section in ('windows', 'linux')] + rows.append(f'| {label} | {cells[0]} | {cells[1]} |') + tools = '\n'.join(rows) + english = _highlights(version, '.md', notes_dir) + DOWNLOAD_EN.format(version=version, tools=tools) + russian = _highlights(version, '.ru.md', notes_dir) + DOWNLOAD_RU.format(version=version, tools=tools) + return f'# phymlGUI {version}\n\n{english}\n---\n\n{russian}' + + +def raxml_source_bundle(source_zip, patch, out): + """A tar.gz with the official RAxML-NG source zip and our Windows patch.""" + folder = 'raxml-ng-2.0.3-source+patch' + with tarfile.open(out, 'w:gz') as t: + t.add(source_zip, arcname=f'{folder}/{os.path.basename(source_zip)}') + t.add(patch, arcname=f'{folder}/raxml-ng-2.0.3-windows.patch') + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest='command', required=True) + sub.add_parser('version').add_argument('ref') + s = sub.add_parser('sha256sums') + s.add_argument('out') + s.add_argument('files', nargs='+') + n = sub.add_parser('notes') + n.add_argument('version') + n.add_argument('out') + r = sub.add_parser('raxml-source') + r.add_argument('source_zip') + r.add_argument('patch') + r.add_argument('out') + args = parser.parse_args(argv) + if args.command == 'version': + try: + print(version_for_ref(args.ref)) + except ValueError as e: + print(e, file=sys.stderr) + return 2 + elif args.command == 'sha256sums': + write_sha256sums(args.files, args.out) + elif args.command == 'notes': + with open(MANIFEST, encoding='utf-8') as f: + manifest = json.load(f) + with open(args.out, 'w', encoding='utf-8', newline='\n') as f: + f.write(release_notes(args.version, manifest)) + else: + raxml_source_bundle(args.source_zip, args.patch, args.out) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/packaging/release_notes/2.0.0.md b/packaging/release_notes/2.0.0.md new file mode 100644 index 0000000..c5f5466 --- /dev/null +++ b/packaging/release_notes/2.0.0.md @@ -0,0 +1,38 @@ +### Overview + +phymlGUI 2.0.0 brings a new window, IQ-TREE as a second engine, an interactive tree viewer and ready-to-run packages for Windows and Linux with all analysis programs included. The program aligns FASTA files with MUSCLE and builds maximum-likelihood trees with PhyML (serial or MPI) or IQ-TREE (ModelFinder, ultrafast bootstrap), for single files or whole folders. + +### New window + +- Two panes: the parameters, recent files, presets and run history on the left; the tree, log, statistics, summary and batch queue on the right. +- Light and dark themes following the system, with an accent colour. +- Every setting is checked before a run, and all problems are listed at once. +- A live CPU monitor for the system and for the analysis process tree; Cancel stops the whole process tree, MPI ranks included. + +### Tree viewer + +- Rectangular, slanted and circular layouts with a distance grid and a scale. +- Support values with threshold highlighting, clade collapsing and rerooting on an outgroup. +- Majority-rule consensus from bootstrap trees and Robinson–Foulds comparison of two trees. +- Export to PNG, SVG and PDF. + +### Engines and runs + +- IQ-TREE 2.4.0: ModelFinder chooses the model (`MFP`); ultrafast bootstrap with the 1000-replicate minimum enforced; the chosen model and the statistics are shown in the window. +- Batch mode with a live per-file queue, retry of the failed files and an Excel summary next to the CSV one. +- Run history (a double click reopens a tree) and parameter presets. +- Command line without the window: `phymlgui-cli.exe` on Windows, the AppImage itself on Linux (`--file`, `--batch`, `--config`, `--version`). + +### Packages + +- Windows zip and Linux AppImage, each with PhyML, IQ-TREE, MUSCLE and RAxML-NG; Python is not required. +- RAxML-NG 2.0.3 is now built natively for Windows from the official sources; its results match the official Linux build. It is included for the command line; an engine in the window is planned. +- Every package is tested before release: alignment and a tree with PhyML and IQ-TREE, an MPI run on Linux, the window opening, and the AppImage on a clean Ubuntu 22.04. + +### Upgrading from 1.2 + +- The Windows archive is now `phymlGUI--windows-x64.zip`; unpack it into a new folder. +- To keep your settings and run history, copy `phyml_config.json` and `phyml_history.json` from the 1.2 folder next to the new `phymlgui.exe`. Settings and history are kept next to the program when that folder is writable, otherwise in `%APPDATA%\phymlGUI` (Windows) or `~/.config/phymlGUI` (Linux). +- The bundled PhyML for Windows is an MPI build: install Microsoft MPI also for serial runs. + +The full list of changes is in [CHANGELOG.md](https://github.com/adrobi/phymlGUI/blob/master/CHANGELOG.md). diff --git a/packaging/release_notes/2.0.0.ru.md b/packaging/release_notes/2.0.0.ru.md new file mode 100644 index 0000000..c0389da --- /dev/null +++ b/packaging/release_notes/2.0.0.ru.md @@ -0,0 +1,38 @@ +### Обзор + +В phymlGUI 2.0.0 — новое окно, IQ-TREE как второй движок, интерактивный просмотрщик деревьев и готовые пакеты для Windows и Linux со всеми программами анализа внутри. Программа выравнивает FASTA-файлы MUSCLE и строит деревья максимального правдоподобия в PhyML (последовательно или через MPI) или IQ-TREE (ModelFinder, ультрабыстрый бутстреп) — для отдельных файлов или целых папок. + +### Новое окно + +- Две панели: слева параметры, недавние файлы, пресеты и история запусков; справа дерево, журнал, статистика, сводка и очередь пакетного режима. +- Светлая и тёмная темы, следующие за системой, с выбором акцентного цвета. +- Все настройки проверяются до запуска, и все ошибки показываются сразу списком. +- Монитор загрузки CPU для системы и для дерева процессов анализа; «Прервать» останавливает всё дерево процессов, включая ранги MPI. + +### Просмотрщик деревьев + +- Прямоугольная, наклонная и круговая раскладки с сеткой расстояний и шкалой. +- Значения поддержек с подсветкой по порогу, сворачивание клад и укоренение по outgroup. +- Консенсус большинства из бутстреп-деревьев и сравнение двух деревьев по Робинсону–Фаулдсу. +- Экспорт в PNG, SVG и PDF. + +### Движки и запуски + +- IQ-TREE 2.4.0: ModelFinder подбирает модель (`MFP`); ультрабыстрый бутстреп с минимумом в 1000 реплик; выбранная модель и статистика показываются в окне. +- Пакетный режим с живой очередью по файлам, повтором неудавшихся и сводкой в Excel рядом с CSV. +- История запусков (двойной клик открывает дерево) и пресеты параметров. +- Командная строка без окна: `phymlgui-cli.exe` под Windows, сам AppImage под Linux (`--file`, `--batch`, `--config`, `--version`). + +### Пакеты + +- Zip для Windows и AppImage для Linux, в каждом PhyML, IQ-TREE, MUSCLE и RAxML-NG; Python не нужен. +- RAxML-NG 2.0.3 теперь собирается под Windows нативно из официальных исходников; результаты совпадают с официальной сборкой для Linux. Он входит в комплект для командной строки; движок в окне запланирован. +- Каждый пакет проверяется перед выпуском: выравнивание и дерево в PhyML и IQ-TREE, MPI-запуск под Linux, открытие окна и AppImage на чистой Ubuntu 22.04. + +### Переход с 1.2 + +- Архив для Windows теперь называется `phymlGUI-<версия>-windows-x64.zip`; распакуйте его в новую папку. +- Чтобы сохранить настройки и историю запусков, скопируйте `phyml_config.json` и `phyml_history.json` из папки 1.2 рядом с новым `phymlgui.exe`. Настройки и история хранятся рядом с программой, если эта папка доступна для записи, иначе в `%APPDATA%\phymlGUI` (Windows) или `~/.config/phymlGUI` (Linux). +- Встроенный PhyML для Windows собран с MPI: Microsoft MPI нужен и для последовательного режима. + +Полный список изменений — в [CHANGELOG.md](https://github.com/adrobi/phymlGUI/blob/master/CHANGELOG.md) (на английском). diff --git a/packaging/smoke_test.py b/packaging/smoke_test.py new file mode 100644 index 0000000..c48ff17 --- /dev/null +++ b/packaging/smoke_test.py @@ -0,0 +1,102 @@ +"""Smoke-test a built phymlGUI package (Windows zip or Linux AppImage/AppDir). + + python packaging/smoke_test.py --package dist/phymlGUI-2.0.0-windows-x64.zip --version 2.0.0 + python packaging/smoke_test.py --package dist/phymlGUI-2.0.0-x86_64.AppImage --version 2.0.0 --mpi + +Checks: --version; a CLI run on unaligned FASTA with the bundled MUSCLE and serial +PhyML; a CLI run with IQ-TREE; optionally an MPI PhyML run (Linux, OpenMPI from +PATH); the window opens offscreen and closes. Settings go to a temporary folder. +""" +import argparse +import json +import os +import subprocess +import sys +import tempfile +import zipfile + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(ROOT, 'tests', 'e2e')) + + +def _launchers(package, work): + """(cli argv prefix, gui argv prefix) for the package.""" + if package.endswith('.zip'): + with zipfile.ZipFile(package) as z: + z.extractall(work) + app = os.path.join(work, 'phymlGUI') + return [os.path.join(app, 'phymlgui-cli.exe')], [os.path.join(app, 'phymlgui.exe')] + if package.endswith('.AppImage'): + os.chmod(package, 0o755) + argv = [os.path.abspath(package), '--appimage-extract-and-run'] + return argv, argv + run = [os.path.join(package, 'AppRun')] # an AppDir + return run, run + + +def _run(argv, env, timeout=600): + r = subprocess.run(argv, capture_output=True, text=True, env=env, timeout=timeout, + encoding='utf-8', errors='replace') + return r.returncode, r.stdout + r.stderr + + +def _analysis(cli, env, work, name, overlay): + from dataset_gen import make_dataset, write_fasta + names, _, unaligned = make_dataset() + folder = os.path.join(work, name) + os.makedirs(folder) + fasta = os.path.join(folder, 'unaligned.fasta') + write_fasta(fasta, names, unaligned) + config = os.path.join(work, f'{name}.json') + with open(config, 'w', encoding='utf-8') as f: + json.dump(overlay, f) + code, out = _run([*cli, '--file', fasta, '--config', config], env) + trees = [f for f in os.listdir(folder) if f.endswith(('_phyml_tree.txt', '.treefile'))] + return code == 0 and bool(trees), out + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--package', required=True) + parser.add_argument('--version', required=True) + parser.add_argument('--mpi', action='store_true', help='also run PhyML-MPI (Linux)') + args = parser.parse_args(argv) + failures = [] + with tempfile.TemporaryDirectory(prefix='phymlgui_smoke_') as work: + cli, gui = _launchers(os.path.abspath(args.package), work) + env = dict(os.environ, PHYMLGUI_DATA_DIR=os.path.join(work, 'data'), + QT_QPA_PLATFORM='offscreen') + + code, out = _run([*cli, '--version'], env, timeout=120) + ok = code == 0 and out.strip().endswith(f'phymlGUI {args.version}') + print(f'{"OK " if ok else "FAIL"} --version: {out.strip()!r}') + failures += [] if ok else ['--version'] + + base = {'datatype': 'nt', 'model': 'HKY85', 'mpi_np': '2', 'use_mpi': False} + checks = [ + # bootstrap without MPI needs the serial build, which only the AppImage bundles + ('phyml', dict(base, engine='phyml', support_method='-4') if args.package.endswith('.zip') + else dict(base, engine='phyml', support_method='bootstrap', bootstrap_replicates='4')), + ('iqtree', dict(base, engine='iqtree', model='MFP', support_method='-4')), + ] + if args.mpi: + checks.append(('phyml-mpi', dict(base, engine='phyml', use_mpi=True, + support_method='bootstrap', bootstrap_replicates='4'))) + for name, overlay in checks: + ok, out = _analysis(cli, env, work, name, overlay) + print(f'{"OK " if ok else "FAIL"} {name} run') + if not ok: + failures.append(name) + print(out[-2000:]) + + code, out = _run([*gui, '--smoke-gui'], env, timeout=120) + print(f'{"OK " if code == 0 else "FAIL"} window opens and closes') + if code != 0: + failures.append('gui') + print(out[-2000:]) + print('SMOKE TEST ' + ('PASSED' if not failures else f'FAILED: {", ".join(failures)}')) + return 0 if not failures else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/packaging/third_party.json b/packaging/third_party.json index b45d1d3..5de00dc 100644 --- a/packaging/third_party.json +++ b/packaging/third_party.json @@ -1,17 +1,102 @@ { - "iqtree": { - "version": "2.4.0", - "url": "https://github.com/iqtree/iqtree2/releases/download/v2.4.0/iqtree-2.4.0-Windows.zip", - "sha256": "93f488a6cfef96ef664b23fefe5be97c2c80ebcae9cd2031c0cac92b467278be", - "extract": { - "iqtree-2.4.0-Windows/bin/iqtree2.exe": "iqtree2.exe", - "iqtree-2.4.0-Windows/bin/libiomp5md.dll": "libiomp5md.dll" + "windows": { + "iqtree": { + "version": "2.4.0", + "url": "https://github.com/iqtree/iqtree2/releases/download/v2.4.0/iqtree-2.4.0-Windows.zip", + "sha256": "93f488a6cfef96ef664b23fefe5be97c2c80ebcae9cd2031c0cac92b467278be", + "extract": { + "iqtree-2.4.0-Windows/bin/iqtree2.exe": "iqtree2.exe", + "iqtree-2.4.0-Windows/bin/libiomp5md.dll": "libiomp5md.dll" + } + }, + "muscle": { + "version": "5.3", + "url": "https://github.com/rcedgar/muscle/releases/download/v5.3/muscle-win64.v5.3.exe", + "sha256": "a86637db99deb3efa1cb6762c4e3160ca33644608270eb167e4214f1f9409711", + "save_as": "muscle.exe" + }, + "phyml": { + "version": "3.3 (MS-MPI build from phymlGUI v1.2)", + "url": "https://github.com/adrobi/phymlGUI/releases/download/v1.2/phymlGUI-1.2-win64.zip", + "sha256": "4258578f2772fe5958513275cd7a64b6230d90795477b55ab156487bf0b08459", + "extract": { + "phymlGUI-1.2/phyml-mpi.exe": "phyml-mpi.exe" + } + }, + "raxml": { + "version": "2.0.3 (native build, packaging/patches/raxml-ng-2.0.3-windows.patch)", + "url": "https://github.com/amkozlov/raxml-ng/releases/download/2.0.3/raxml-ng_v2.0.3_source.zip", + "sha256": "7cd3fa6b1de373777d688032c9ec6731078b67e6a812d7a1d779295d016843d2", + "build": [ + "bash", + "packaging/build_raxml_windows.sh" + ], + "outputs": [ + "raxml-ng.exe" + ] } }, - "muscle": { - "version": "5.3", - "url": "https://github.com/rcedgar/muscle/releases/download/v5.3/muscle-win64.v5.3.exe", - "sha256": "a86637db99deb3efa1cb6762c4e3160ca33644608270eb167e4214f1f9409711", - "save_as": "muscle.exe" + "linux": { + "iqtree": { + "version": "2.4.0", + "url": "https://github.com/iqtree/iqtree2/releases/download/v2.4.0/iqtree-2.4.0-Linux-intel.tar.gz", + "sha256": "49c58e26f192dd5e0045110cee0af151203dfa0bd79fa03d01fcfc3e44ca2f1b", + "extract": { + "iqtree-2.4.0-Linux-intel/bin/iqtree2": "iqtree2" + }, + "mode": "755" + }, + "muscle": { + "version": "5.3", + "url": "https://github.com/rcedgar/muscle/releases/download/v5.3/muscle-linux-x86.v5.3", + "sha256": "318abeb951d786a3e2532714cc81ad3b3d8f79a2b517dc31316eeb5b694db2bc", + "save_as": "muscle", + "mode": "755" + }, + "phyml": { + "version": "3.3.20260528", + "url": "https://github.com/stephaneguindon/phyml/archive/refs/tags/v3.3.20260528.tar.gz", + "sha256": "42451c46de7cd01f8ca67d657766acf5b02f61027127bdda87efe3cdb422b297", + "build": [ + "bash", + "packaging/build_phyml_linux.sh" + ], + "outputs": [ + "phyml", + "phyml-mpi" + ] + }, + "raxml": { + "version": "2.0.3", + "url": "https://github.com/amkozlov/raxml-ng/releases/download/2.0.3/raxml-ng_v2.0.3_linux_x86_64.zip", + "sha256": "d660cebe2a083de6c20d9354968803b8fd10be206f625be2393997d0abe98105", + "extract": { + "raxml-ng": "raxml-ng" + }, + "mode": "755" + } + }, + "build_tools": { + "appimagetool": { + "version": "1.9.1", + "url": "https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage", + "sha256": "ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0", + "save_as": "appimagetool", + "mode": "755" + }, + "appimage_runtime": { + "version": "20251108", + "url": "https://github.com/AppImage/type2-runtime/releases/download/20251108/runtime-x86_64", + "sha256": "2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d", + "save_as": "runtime-x86_64" + } + }, + "windows_runtime": { + "msmpi": { + "version": "10.1.1 (the bundled phyml-mpi.exe links msmpi.dll even in serial mode)", + "url": "https://github.com/microsoft/Microsoft-MPI/releases/download/v10.1.1/msmpisetup.exe", + "sha256": "7308fd15e437d6829fd9c6ec5d801380faa3bff6e66e1dee3e8e005106fb6a68", + "save_as": "msmpisetup.exe" + } } } diff --git a/phyml_core/__init__.py b/phyml_core/__init__.py index f9d5722..864bab4 100644 --- a/phyml_core/__init__.py +++ b/phyml_core/__init__.py @@ -1,3 +1,3 @@ """phymlGUI core: settings, runner, engines, i18n and helpers (no GUI code here).""" -__version__ = "1.2" +__version__ = "2.0.0" diff --git a/phyml_core/cli.py b/phyml_core/cli.py index 159e2b0..31cac0c 100644 --- a/phyml_core/cli.py +++ b/phyml_core/cli.py @@ -17,6 +17,7 @@ from . import settings from .i18n import _t +from . import __version__ from .paths import AppPaths from .runner import PipelineRunner @@ -94,6 +95,7 @@ def main(argv=None) -> int: parser.add_argument('--file', help='single FASTA file') parser.add_argument('--batch', help='folder with FASTA files') parser.add_argument('--config', help='JSON object laid over the saved settings') + parser.add_argument('--version', action='version', version=f'phymlGUI {__version__}') try: args = parser.parse_args(argv) if not args.file and not args.batch: diff --git a/phyml_core/engines/iqtree.py b/phyml_core/engines/iqtree.py index c800561..b4e8d21 100644 --- a/phyml_core/engines/iqtree.py +++ b/phyml_core/engines/iqtree.py @@ -5,12 +5,13 @@ from ..launchers import Mpi, Native from ..paths import AppPaths +from ..utils import exe_name from .base import Engine, EngineOutputs, Issue, param_text, positive_int NT_MODELS = ("MFP", "GTR+F+G4", "HKY+F+G4", "TIM2+F+G4", "TVM+F+G4") AA_MODELS = ("MFP", "LG+G4", "WAG+G4", "JTT+G4", "mtREV+G4") -EXECUTABLE_NAMES = ('iqtree2.exe',) +EXECUTABLE_BASES = ('iqtree2',) UFBOOT_MIN_REPLICATES = 1000 @@ -87,7 +88,7 @@ def normalize(self, params: dict) -> Tuple[dict, List[Issue]]: return normalized, notes def executable(self, params: dict, paths: AppPaths) -> str: - return paths.find_tool(params.get('iqtree_path', ''), EXECUTABLE_NAMES) + return paths.find_tool(params.get('iqtree_path', ''), tuple(exe_name(b) for b in EXECUTABLE_BASES)) def command(self, exe: str, phylip: str, params: dict) -> List[str]: argv = [exe, '-s', phylip, '--prefix', _prefix(phylip), '-redo', diff --git a/phyml_core/engines/phyml.py b/phyml_core/engines/phyml.py index 67dcae1..e9c911c 100644 --- a/phyml_core/engines/phyml.py +++ b/phyml_core/engines/phyml.py @@ -6,7 +6,7 @@ from ..launchers import Mpi, Native from ..paths import AppPaths -from ..utils import is_mpi_binary +from ..utils import default_mpirun, exe_name, is_mpi_binary from .base import (Engine, EngineOutputs, Issue, param_text, parse_decimal, positive_int, positive_number) @@ -14,7 +14,10 @@ AA_MODELS = ("LG", "WAG", "JTT", "MtREV", "Dayhoff", "DCMut", "RtREV", "CPREV", "VT", "Blosum62", "MtMam", "MtArt", "HIVw", "HIVb", "custom") -EXECUTABLE_NAMES = ('phyml-mpi.exe', 'phyml.exe') +# the build matching the MPI mode comes first; the other one is the fallback +# (the Windows zip ships only phyml-mpi.exe, the AppImage ships both) +EXECUTABLE_BASES = ('phyml', 'phyml-mpi') +MPI_EXECUTABLE_BASES = ('phyml-mpi', 'phyml') def parse_phyml_stats(stats_path: str) -> dict: @@ -73,7 +76,7 @@ def validate(self, params: dict, paths: AppPaths) -> List[Issue]: issues.append(Issue('error', 'mpi_boot_msg')) if use_mpi: - mpiexec = param_text(params, 'mpirun_path', 'mpiexec') or 'mpiexec' + mpiexec = param_text(params, 'mpirun_path', default_mpirun()) or default_mpirun() if not _mpiexec_found(mpiexec): issues.append(Issue('error', 'val_no_mpiexec', (mpiexec,))) @@ -102,7 +105,8 @@ def normalize(self, params: dict) -> Tuple[dict, List[Issue]]: return dict(params), [] def executable(self, params: dict, paths: AppPaths) -> str: - return paths.find_tool(params.get('phyml_path', ''), EXECUTABLE_NAMES) + bases = MPI_EXECUTABLE_BASES if params.get('use_mpi') else EXECUTABLE_BASES + return paths.find_tool(params.get('phyml_path', ''), tuple(exe_name(b) for b in bases)) def command(self, exe: str, phylip: str, params: dict) -> List[str]: argv = [exe, '-i', phylip, @@ -135,5 +139,5 @@ def launcher(self, params: dict) -> Union[Native, Mpi]: """Call validate() first: a bad mpi_np raises ValueError here.""" if not params.get('use_mpi', False): return Native() - mpiexec = param_text(params, 'mpirun_path', 'mpiexec') or 'mpiexec' + mpiexec = param_text(params, 'mpirun_path', default_mpirun()) or default_mpirun() return Mpi((mpiexec and shutil.which(mpiexec)) or mpiexec, int(param_text(params, 'mpi_np', '2'))) diff --git a/phyml_core/paths.py b/phyml_core/paths.py index 5ffd95a..f720377 100644 --- a/phyml_core/paths.py +++ b/phyml_core/paths.py @@ -1,5 +1,6 @@ """Where phymlGUI keeps its program files and its user data (no Qt, no cwd dependence).""" import os +import shutil import sys import tempfile from dataclasses import dataclass @@ -82,7 +83,8 @@ def from_stored(self, stored: str) -> str: def find_tool(self, stored: str, names: tuple) -> str: """The configured tool if it exists, else the first of names in bin/, else the - configured path again (so the error message names what the user set).""" + first of names on PATH, else the configured path again (so the error message + names what the user set).""" configured = self.from_stored(stored) if configured and os.path.isfile(configured): return configured @@ -90,24 +92,35 @@ def find_tool(self, stored: str, names: tuple) -> str: candidate = os.path.join(self.bin_dir, name) if os.path.isfile(candidate): return candidate + for name in names: + found = shutil.which(name) + if found: + return found return configured @classmethod def detect(cls, env: Optional[Mapping[str, str]] = None, is_writable: Optional[Callable[[str], bool]] = None, - app_dir: Optional[str] = None) -> 'AppPaths': + app_dir: Optional[str] = None, + platform: Optional[str] = None) -> 'AppPaths': """Pick the data folder: $PHYMLGUI_DATA_DIR, then app_dir if writable, then - %APPDATA%\\phymlGUI. The chosen folder is created.""" + %APPDATA%\\phymlGUI on Windows or $XDG_CONFIG_HOME/phymlGUI (~/.config/phymlGUI) + elsewhere, e.g. inside a read-only AppImage. The chosen folder is created.""" env = os.environ if env is None else env + platform = os.name if platform is None else platform is_writable = is_writable or _dir_is_writable app = default_app_dir() if app_dir is None else app_dir override = env.get(DATA_DIR_ENV) if override: data = override - elif is_writable(app): + elif not env.get('APPIMAGE') and is_writable(app): # an AppImage's folder is throwaway data = app - else: + elif platform == 'nt': base = env.get('APPDATA') or os.path.join(os.path.expanduser('~'), 'AppData', 'Roaming') data = os.path.join(base, APPDATA_SUBDIR) + else: + home = env.get('HOME') or os.path.expanduser('~') + base = env.get('XDG_CONFIG_HOME') or os.path.join(home, '.config') + data = os.path.join(base, APPDATA_SUBDIR) os.makedirs(data, exist_ok=True) return cls(app_dir=app, data_dir=data) diff --git a/phyml_core/runner.py b/phyml_core/runner.py index ca48122..2ad7503 100644 --- a/phyml_core/runner.py +++ b/phyml_core/runner.py @@ -13,7 +13,7 @@ from dataclasses import dataclass, field from typing import Callable, List, Literal, Mapping, Optional, Tuple -from . import alignment +from . import alignment, utils from .alignment import AlignmentError from .engines import ENGINES from .engines.base import Engine, format_issue, param_text @@ -29,7 +29,7 @@ LEVELS = ('error', 'success', 'system', 'out') STATUSES = ('pending', 'running', 'ok', 'fail', 'cancelled') STAT_KEYS = ('model', 'taxa', 'lnL', 'gamma', 'tstv', 'time') -MUSCLE_NAMES = ('muscle.exe',) +MUSCLE_BASES = ('muscle',) KILL_TIMEOUT = 3.0 # seconds between terminate and kill _CREATION_FLAGS = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0 @@ -218,7 +218,7 @@ def _process(self, fasta: str) -> Tuple[str, dict]: def _align(self, fasta: str) -> str: """Run MUSCLE (version 5 syntax, then version 3); returns the aligned file.""" - muscle = self._paths.find_tool(self._params.get('muscle_path') or '', MUSCLE_NAMES) + muscle = self._paths.find_tool(self._params.get('muscle_path') or '', tuple(utils.exe_name(b) for b in MUSCLE_BASES)) if not muscle or not os.path.isfile(muscle): raise _FileFailed(_t('err_muscle')) aligned = alignment.aligned_path(fasta) @@ -282,6 +282,7 @@ def _execute(self, argv: List[str], cancel_key: str, show_output: bool) -> int: raise _Cancelled(cancel_key) proc = subprocess.Popen(argv, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, errors='replace', + env=utils.child_env(), creationflags=_CREATION_FLAGS) self._proc = proc try: diff --git a/phyml_core/settings.py b/phyml_core/settings.py index 88289a2..3915ab1 100644 --- a/phyml_core/settings.py +++ b/phyml_core/settings.py @@ -8,6 +8,7 @@ from .i18n import _t from .paths import CONFIG_NAME, HISTORY_NAME, AppPaths +from . import utils from .utils import detect_cores SCHEMA_VERSION = 2 @@ -153,6 +154,10 @@ def from_disk(cfg: dict, paths: AppPaths) -> dict: out[key] = paths.from_stored(value) if isinstance(out.get('recent'), list): out['recent'] = [paths.from_stored(p) if isinstance(p, str) else p for p in out['recent']] + # a config carried over from Windows names MS-MPI's mpiexec; OpenMPI ships mpirun + if (not utils.IS_WINDOWS and out.get('mpirun_path') == 'mpiexec' + and shutil.which('mpiexec') is None): + out['mpirun_path'] = utils.default_mpirun() return out diff --git a/phyml_core/utils.py b/phyml_core/utils.py index f219d00..3812094 100644 --- a/phyml_core/utils.py +++ b/phyml_core/utils.py @@ -9,6 +9,35 @@ psutil = None +IS_WINDOWS = os.name == 'nt' + + +def exe_name(base): + """Platform file name of a tool: 'muscle' -> 'muscle.exe' on Windows.""" + return base + '.exe' if IS_WINDOWS else base + + +def default_mpirun(): + """The MPI launcher looked up through PATH: MS-MPI's mpiexec or OpenMPI's mpirun.""" + return 'mpiexec' if IS_WINDOWS else 'mpirun' + + +def child_env(environ=None, frozen=None): + """Environment for programs started by the app. A frozen Linux build's bootloader + puts its _internal folder first on LD_LIBRARY_PATH (the user's value is kept in + LD_LIBRARY_PATH_ORIG); child programs must not load those bundled libraries.""" + env = dict(os.environ if environ is None else environ) + if frozen is None: + frozen = getattr(sys, 'frozen', False) + if frozen and not IS_WINDOWS: + original = env.pop('LD_LIBRARY_PATH_ORIG', None) + if original is None: + env.pop('LD_LIBRARY_PATH', None) + else: + env['LD_LIBRARY_PATH'] = original + return env + + def resource_path(relative_path): """Absolute path to a bundled resource: inside the PyInstaller bundle when frozen, otherwise next to the program (never relative to the current working folder).""" diff --git a/phyml_ui/main_window.py b/phyml_ui/main_window.py index 5f62231..7d9391f 100644 --- a/phyml_ui/main_window.py +++ b/phyml_ui/main_window.py @@ -6,10 +6,10 @@ from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QComboBox, QLineEdit, QPushButton, QMessageBox, QCheckBox, QSplitter, QScrollArea, QInputDialog, QApplication) -from PySide6.QtCore import Qt -from PySide6.QtGui import QIcon +from PySide6.QtCore import Qt, QUrl +from PySide6.QtGui import QDesktopServices, QIcon -from phyml_core import __version__, presets, report, settings +from phyml_core import __version__, presets, report, settings, utils from phyml_core.engines import ENGINES from phyml_core.engines.base import format_issue from phyml_core.history import HistoryStore, best_tree, new_record @@ -391,7 +391,18 @@ def _retry_failed(self): def _open_output_dir(self): if self._last_out_dir and os.path.isdir(self._last_out_dir): - os.startfile(self._last_out_dir) + if utils.IS_WINDOWS: + os.startfile(self._last_out_dir) + else: # xdg-open, also from inside an AppImage: it inherits os.environ, + # so hand it the environment without the bundle's LD_LIBRARY_PATH + saved = dict(os.environ) + os.environ.clear() + os.environ.update(utils.child_env()) + try: + QDesktopServices.openUrl(QUrl.fromLocalFile(self._last_out_dir)) + finally: + os.environ.clear() + os.environ.update(saved) # ---------- history ---------- diff --git a/phyml_ui/themes.py b/phyml_ui/themes.py index f55f885..14f191e 100644 --- a/phyml_ui/themes.py +++ b/phyml_ui/themes.py @@ -31,7 +31,20 @@ def system_theme(): return 'dark' if value == 0 else 'light' except OSError: pass - return 'light' + return 'light' + return _palette_theme() + + +def _palette_theme(): + """'dark' when the desktop's Qt palette has a dark window colour (Linux).""" + try: + from PySide6.QtGui import QGuiApplication + except ImportError: + return 'light' + app = QGuiApplication.instance() + if app is None: + return 'light' + return 'dark' if app.palette().window().color().lightness() < 128 else 'light' def system_accent(): diff --git a/phymlgui.py b/phymlgui.py index 52cc3c0..37b70ef 100644 --- a/phymlgui.py +++ b/phymlgui.py @@ -7,7 +7,8 @@ def main(): - if len(sys.argv) > 1: + smoke_gui = sys.argv[1:] == ['--smoke-gui'] # packaging check: open and close the window + if len(sys.argv) > 1 and not smoke_gui: from phyml_core.cli import main as cli_main sys.exit(cli_main()) from PySide6.QtWidgets import QApplication @@ -20,6 +21,9 @@ def main(): QMessageBox.critical(None, 'phymlGUI', str(e)) sys.exit(1) window.show() + if smoke_gui: + from PySide6.QtCore import QTimer + QTimer.singleShot(1500, window.close) sys.exit(app.exec()) diff --git a/requirements-dev.txt b/requirements-dev.txt index f6c9b3a..f0e01a6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,3 @@ pytest>=8 pytest-qt>=4.4 +pyinstaller>=6 diff --git a/scripts/fetch_third_party.py b/scripts/fetch_third_party.py index 907e2df..e40c0d6 100644 --- a/scripts/fetch_third_party.py +++ b/scripts/fetch_third_party.py @@ -1,14 +1,20 @@ -"""Download the pinned third-party binaries (IQ-TREE, MUSCLE) into a folder. +"""Download (or build) the pinned third-party programs into a folder. -Every download is verified against the SHA256 in packaging/third_party.json -before anything is written to the destination. +packaging/third_party.json has one section per platform ("windows", "linux", +and "build_tools" for packaging helpers). Every download is verified against +its SHA256 before anything is written to the destination. An entry then +either extracts listed members of a zip/tar.gz ("extract"), saves the file +under a new name ("save_as"), or runs a build command ("build": argv that +receives the downloaded archive and the destination) and checks "outputs". """ import argparse import hashlib import json import os import shutil +import subprocess import sys +import tarfile import tempfile import urllib.request import zipfile @@ -17,6 +23,10 @@ MANIFEST = os.path.join(ROOT, 'packaging', 'third_party.json') +def default_platform(): + return 'windows' if os.name == 'nt' else 'linux' + + def _download(url, target): sha = hashlib.sha256() with urllib.request.urlopen(url, timeout=120) as resp, open(target, 'wb') as out: @@ -29,6 +39,40 @@ def _download(url, target): return sha.hexdigest() +def _extract(archive, members, dest): + written = [] + if zipfile.is_zipfile(archive): + with zipfile.ZipFile(archive) as z: + for member, out_name in members.items(): + target = os.path.join(dest, out_name) + with z.open(member) as src, open(target, 'wb') as out: + shutil.copyfileobj(src, out) + written.append(target) + else: + with tarfile.open(archive) as t: + for member, out_name in members.items(): + src = t.extractfile(member) + if src is None: + raise KeyError(member) + target = os.path.join(dest, out_name) + with src, open(target, 'wb') as out: + shutil.copyfileobj(src, out) + written.append(target) + return written + + +def _build(spec, archive, dest): + command = list(spec['build']) + if command[0] == 'bash' and os.environ.get('PHYMLGUI_BASH'): + command[0] = os.environ['PHYMLGUI_BASH'] # e.g. MSYS2's bash on Windows + subprocess.run([*command, archive, dest], check=True, cwd=ROOT) + written = [os.path.join(dest, name) for name in spec.get('outputs', [])] + missing = [p for p in written if not os.path.isfile(p)] + if missing: + raise ValueError(f'build did not produce: {", ".join(missing)}') + return written + + def fetch(name, spec, dest): """Download spec['url'], check SHA256, put the files into dest; return their paths.""" with tempfile.TemporaryDirectory(prefix='phymlgui_fetch_') as tmp: @@ -38,38 +82,44 @@ def fetch(name, spec, dest): if actual != expected: raise ValueError(f'{name}: SHA256 mismatch: expected {expected}, got {actual}') os.makedirs(dest, exist_ok=True) - written = [] - if 'extract' in spec: - with zipfile.ZipFile(archive) as z: - for member, out_name in spec['extract'].items(): - target = os.path.join(dest, out_name) - with z.open(member) as src, open(target, 'wb') as out: - shutil.copyfileobj(src, out) - written.append(target) + if 'build' in spec: + written = _build(spec, archive, os.path.abspath(dest)) + elif 'extract' in spec: + written = _extract(archive, spec['extract'], dest) else: target = os.path.join(dest, spec['save_as']) shutil.copyfile(archive, target) - written.append(target) + written = [target] + if spec.get('mode') and os.name != 'nt': + for path in written: + os.chmod(path, int(spec['mode'], 8)) return written def main(argv=None): parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--platform', default=default_platform(), + help='manifest section: windows, linux or build_tools') parser.add_argument('--dest', default='bin') - parser.add_argument('--only', action='append', choices=None, - help='fetch only this tool (repeatable)') + parser.add_argument('--only', action='append', + help='fetch only this tool (repeatable, comma lists allowed)') args = parser.parse_args(argv) with open(MANIFEST, encoding='utf-8') as f: manifest = json.load(f) - names = args.only or list(manifest) - unknown = [n for n in names if n not in manifest] + section = manifest.get(args.platform) + if section is None: + print(f'unknown platform section: {args.platform}', file=sys.stderr) + return 2 + names = [n for item in (args.only or []) for n in item.split(',') if n] or list(section) + unknown = [n for n in names if n not in section] if unknown: print(f'unknown tool(s): {", ".join(unknown)}', file=sys.stderr) return 2 for name in names: try: - paths = fetch(name, manifest[name], args.dest) - except (ValueError, OSError, KeyError, zipfile.BadZipFile) as exc: + paths = fetch(name, section[name], args.dest) + except (ValueError, OSError, KeyError, zipfile.BadZipFile, tarfile.TarError, + subprocess.CalledProcessError) as exc: print(f'{name}: {exc}', file=sys.stderr) return 1 for p in paths: diff --git a/scripts/make_screenshots.py b/scripts/make_screenshots.py new file mode 100644 index 0000000..adafef3 --- /dev/null +++ b/scripts/make_screenshots.py @@ -0,0 +1,127 @@ +"""Screenshots for the README: real runs of the bundled tools in a throwaway data folder. + + python scripts/make_screenshots.py --fasta testrun/cytb_mammals.fasta --out docs/screenshots + +The inputs are copied to testrun/screenshots (the path shows in the window). +Writes, for English and Russian (suffix _ru): main.png (PhyML with MPI bootstrap, +light theme), dark.png (the same tree, slanted layout, dark theme) and iqtree.png +(IQ-TREE with ModelFinder, statistics tab), all rooted on --outgroup. The user's settings and +history are never touched: PHYMLGUI_DATA_DIR points to a temporary folder. +Tools come from --bin (default: build-bin, then bin). +""" +import argparse +import json +import os +import shutil +import subprocess +import sys +import tempfile +import time + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +WINDOW_SIZE = (1500, 920) + + +def _tool(bins, name): + for folder in bins: + path = os.path.join(ROOT, folder, name) + if os.path.isfile(path): + return path + sys.exit(f'{name} not found in {bins}') + + +def _config(bins, theme, engine): + mpiexec = shutil.which('mpiexec') or r'C:\Program Files\Microsoft MPI\Bin\mpiexec.exe' + return { + 'engine': engine, + 'phyml_path': _tool(bins, 'phyml-mpi.exe'), + 'iqtree_path': _tool(bins, 'iqtree2.exe'), + 'muscle_path': _tool(bins, 'muscle.exe'), + 'mpirun_path': mpiexec, + 'use_mpi': engine == 'phyml', + 'mpi_np': '4', + 'support_method': 'bootstrap', + 'bootstrap_replicates': '100', + 'model': 'HKY85' if engine == 'phyml' else 'MFP', + 'theme': theme, + 'accent': 'green', + } + + +def shoot(lang, theme, engine, fasta, bins, out, name, layout='rect', tab=0, outgroup=None): + """One screenshot in a fresh process: the language is fixed at import time.""" + with tempfile.TemporaryDirectory(prefix='phymlgui_shots_') as tmp: + data = os.path.join(tmp, 'data') + os.makedirs(data) + # the input path shows in the window and the log: keep the user's home out of it + work = os.path.join(ROOT, 'testrun', 'screenshots') + shutil.rmtree(work, ignore_errors=True) + os.makedirs(work) + with open(os.path.join(data, 'phyml_config.json'), 'w', encoding='utf-8') as f: + json.dump(_config(bins, theme, engine), f) + target = os.path.join(work, os.path.basename(fasta)) + shutil.copy(fasta, target) + job = dict(lang=lang, fasta=target, png=os.path.join(out, name), layout=layout, tab=tab, + outgroup=outgroup) + env = dict(os.environ, PHYMLGUI_DATA_DIR=data, PHYMLGUI_SHOT=json.dumps(job)) + subprocess.run([sys.executable, os.path.abspath(__file__), '--child'], env=env, check=True) + + +def child(): + job = json.loads(os.environ['PHYMLGUI_SHOT']) + sys.path.insert(0, ROOT) + from phyml_core import i18n + i18n.LANG = job['lang'] + from PySide6.QtWidgets import QApplication + from phyml_ui.main_window import PhyMLApp + app = QApplication(sys.argv) + window = PhyMLApp() + window.resize(*WINDOW_SIZE) + window.move(-WINDOW_SIZE[0] - 200, 0) # off-screen: nothing flashes on the desktop + window.show() + window.fasta_path_input.setText(job['fasta']) + window.btn_run.click() + deadline = time.time() + 600 + while not window.btn_run.isEnabled(): + app.processEvents() + time.sleep(0.05) + if time.time() > deadline: + sys.exit('the run did not finish') + combo = window.tree_view.layout_combo + combo.setCurrentIndex(combo.findData(job['layout'])) + if job['outgroup']: + combo = window.tree_view.outgroup_combo + combo.setCurrentIndex(combo.findData(job['outgroup'])) + window.result_tabs.setCurrentIndex(job['tab']) + for _ in range(40): + app.processEvents() + time.sleep(0.025) + window.grab().save(job['png']) + print('wrote', job['png']) + window.close() + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--fasta', default=os.path.join(ROOT, 'testrun', 'cytb_mammals.fasta')) + parser.add_argument('--out', default=os.path.join(ROOT, 'docs', 'screenshots')) + parser.add_argument('--bin', action='append', help='folder with the tools (repeatable)') + parser.add_argument('--outgroup', default='Mus_musculus', help='taxon to root the trees on') + parser.add_argument('--child', action='store_true', help=argparse.SUPPRESS) + args = parser.parse_args(argv) + if args.child: + child() + return 0 + bins = args.bin or ['build-bin', 'bin'] + os.makedirs(args.out, exist_ok=True) + fasta = os.path.abspath(args.fasta) + for lang, suffix in (('en', ''), ('ru', '_ru')): + common = dict(fasta=fasta, bins=bins, out=args.out, outgroup=args.outgroup) + shoot(lang, 'light', 'phyml', name=f'main{suffix}.png', **common) + shoot(lang, 'dark', 'phyml', name=f'dark{suffix}.png', layout='slanted', **common) + shoot(lang, 'light', 'iqtree', name=f'iqtree{suffix}.png', tab=1, **common) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tests/conftest.py b/tests/conftest.py index 9effe55..556d7d5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ os.environ.setdefault('QT_QPA_PLATFORM', 'offscreen') import hashlib +import shutil import sys from pathlib import Path @@ -12,16 +13,17 @@ sys.path.insert(0, str(ROOT)) _BINARIES = { - # kind: (env var, bin/ file name, extra local locations) - 'phyml': ('PHYMLGUI_PHYML', 'phyml-mpi.exe', + # kind: (env var, tool base name, extra local locations); the base name gets + # '.exe' on Windows (utils.exe_name) and is also looked up on PATH + 'phyml': ('PHYMLGUI_PHYML', 'phyml-mpi', [ROOT / 'phymlgui v1.1' / 'phyml-mpi.exe']), - 'mpiexec': ('PHYMLGUI_MPIEXEC', 'mpiexec.exe', + 'mpiexec': ('PHYMLGUI_MPIEXEC', 'mpiexec' if os.name == 'nt' else 'mpirun', [Path(r'C:\Program Files\Microsoft MPI\Bin\mpiexec.exe')]), - 'iqtree': ('PHYMLGUI_IQTREE', 'iqtree2.exe', + 'iqtree': ('PHYMLGUI_IQTREE', 'iqtree2', [ROOT / 'tools' / 'iqtree2' / 'iqtree2.exe']), - 'muscle': ('PHYMLGUI_MUSCLE', 'muscle.exe', + 'muscle': ('PHYMLGUI_MUSCLE', 'muscle', [ROOT / 'tools' / 'muscle.exe']), - 'raxml': ('PHYMLGUI_RAXML', 'raxml-ng.exe', []), + 'raxml': ('PHYMLGUI_RAXML', 'raxml-ng', []), } # marker name -> binary kind (needs_mpi is satisfied by mpiexec) @@ -34,17 +36,20 @@ def find_binary(kind): - """Path to an analysis binary or None: env var, then ROOT/bin, then local installs.""" - env_var, bin_name, local = _BINARIES[kind] + """Path to an analysis binary or None: env var, then ROOT/bin, then local installs, + then PATH.""" + from phyml_core.utils import exe_name + env_var, base, local = _BINARIES[kind] candidates = [] if os.environ.get(env_var): candidates.append(Path(os.environ[env_var])) - candidates.append(ROOT / 'bin' / bin_name) - candidates.extend(local) + candidates.append(ROOT / 'bin' / exe_name(base)) + if os.name == 'nt': # the extra local locations are Windows installs + candidates.extend(local) for path in candidates: if path.is_file(): return str(path) - return None + return shutil.which(exe_name(base)) def pytest_runtest_setup(item): diff --git a/tests/e2e/dataset_gen.py b/tests/e2e/dataset_gen.py new file mode 100644 index 0000000..277a513 --- /dev/null +++ b/tests/e2e/dataset_gen.py @@ -0,0 +1,37 @@ +"""The synthetic two-clade dataset, without pytest (release scripts use it too). + +Two clades A=(A1..A4) and B=(B1..B4), so a finished run can be checked for the A|B split. +""" +import random + + +def evolve(seq, rate, rng): + bases = 'ACGT' + return ''.join(b if rng.random() > rate else rng.choice(bases) for b in seq) + + +def make_dataset(): + """(names, aligned sequences, unaligned sequences) of the two-clade dataset.""" + rng = random.Random(42) + ancestor = ''.join(rng.choice('ACGT') for _ in range(600)) + names, seqs_aligned, seqs_unaligned = [], [], [] + for clade, clade_rate in (('A', 0.16), ('B', 0.18)): + clade_consensus = evolve(ancestor, clade_rate, rng) + for i in range(1, 5): + s = evolve(clade_consensus, 0.03, rng) + name = f'{clade}{i}_testsp' + names.append(name) + seqs_aligned.append(s) + # unaligned variant: drop a random block from most sequences + if rng.random() < 0.8: + start = rng.randrange(50, 500) + ln = rng.randrange(8, 15) + s = s[:start] + s[start + ln:] + seqs_unaligned.append(s) + return names, seqs_aligned, seqs_unaligned + + +def write_fasta(path, names, seqs): + with open(path, 'w') as f: + for n, s in zip(names, seqs): + f.write(f'>{n}\n{s}\n') diff --git a/tests/e2e/datasets.py b/tests/e2e/datasets.py index 8fd7dfd..b0f861e 100644 --- a/tests/e2e/datasets.py +++ b/tests/e2e/datasets.py @@ -3,41 +3,9 @@ The dataset has a known topology: two clades A=(A1..A4) and B=(B1..B4), so a finished run can be checked for the A|B split. """ -import random - import pytest - -def evolve(seq, rate, rng): - bases = 'ACGT' - return ''.join(b if rng.random() > rate else rng.choice(bases) for b in seq) - - -def make_dataset(): - """(names, aligned sequences, unaligned sequences) of the two-clade dataset.""" - rng = random.Random(42) - ancestor = ''.join(rng.choice('ACGT') for _ in range(600)) - names, seqs_aligned, seqs_unaligned = [], [], [] - for clade, clade_rate in (('A', 0.16), ('B', 0.18)): - clade_consensus = evolve(ancestor, clade_rate, rng) - for i in range(1, 5): - s = evolve(clade_consensus, 0.03, rng) - name = f'{clade}{i}_testsp' - names.append(name) - seqs_aligned.append(s) - # unaligned variant: drop a random block from most sequences - if rng.random() < 0.8: - start = rng.randrange(50, 500) - ln = rng.randrange(8, 15) - s = s[:start] + s[start + ln:] - seqs_unaligned.append(s) - return names, seqs_aligned, seqs_unaligned - - -def write_fasta(path, names, seqs): - with open(path, 'w') as f: - for n, s in zip(names, seqs): - f.write(f'>{n}\n{s}\n') +from dataset_gen import evolve, make_dataset, write_fasta # noqa: F401 (re-exported) @pytest.fixture diff --git a/tests/gui/test_main_window.py b/tests/gui/test_main_window.py index 917f65c..b1ffae9 100644 --- a/tests/gui/test_main_window.py +++ b/tests/gui/test_main_window.py @@ -1,4 +1,6 @@ """GUI behaviour of PhyMLApp that needs no analysis binaries.""" +import os + import pytest from PySide6.QtWidgets import QSizePolicy @@ -394,3 +396,31 @@ def cancel(): assert threading.main_thread() in callers, 'close() left the kill to a daemon thread' alive = [p.pid for p in procs if p.is_running()] assert alive == [], f'processes left after close: {alive}' + + +def test_open_output_dir_uses_qdesktopservices_off_windows(window, tmp_path, monkeypatch): + from PySide6.QtGui import QDesktopServices + from phyml_core import utils + opened = [] + monkeypatch.setattr(utils, 'IS_WINDOWS', False) + monkeypatch.setattr(QDesktopServices, 'openUrl', staticmethod(lambda url: opened.append(url) or True)) + window._last_out_dir = str(tmp_path) + window._open_output_dir() + assert len(opened) == 1 + assert os.path.normcase(os.path.abspath(opened[0].toLocalFile())) == os.path.normcase(str(tmp_path)) + + +def test_open_output_dir_starts_xdg_open_with_the_child_env(window, tmp_path, monkeypatch): + """xdg-open inherits the process environment: it must not see the bundle's LD_LIBRARY_PATH.""" + from PySide6.QtGui import QDesktopServices + from phyml_core import utils + seen = [] + monkeypatch.setattr(utils, 'IS_WINDOWS', False) + monkeypatch.setattr(utils, 'child_env', lambda: {'PHYMLGUI_CHILD': '1'}) + monkeypatch.setattr(QDesktopServices, 'openUrl', + staticmethod(lambda url: seen.append(dict(os.environ)) or True)) + before = dict(os.environ) + window._last_out_dir = str(tmp_path) + window._open_output_dir() + assert seen == [{'PHYMLGUI_CHILD': '1'}] + assert dict(os.environ) == before diff --git a/tests/gui/test_main_window_storage.py b/tests/gui/test_main_window_storage.py index 823e204..9c0b1bc 100644 --- a/tests/gui/test_main_window_storage.py +++ b/tests/gui/test_main_window_storage.py @@ -206,6 +206,8 @@ def test_bare_mpiexec_is_resolved_through_path(data_dir, make_window, monkeypatc def test_bare_mpiexec_kept_when_not_found(data_dir, make_window, monkeypatch): + from phyml_core import utils + monkeypatch.setattr(utils, 'IS_WINDOWS', True) # Windows semantics: mpiexec stays monkeypatch.setattr(shutil, 'which', lambda name: None) w = make_window() assert w.mpirun_path_input.text() == 'mpiexec' diff --git a/tests/gui/test_themes.py b/tests/gui/test_themes.py index ddcc339..248e6b4 100644 --- a/tests/gui/test_themes.py +++ b/tests/gui/test_themes.py @@ -8,3 +8,22 @@ def test_resolve_qss_and_role_colors(): assert accent in qss and 'QPushButton#btn_run' in qss assert role_colors(theme, accent)['out'] == '#E6E8EB' assert system_theme() in ('light', 'dark') + + +def test_system_theme_without_winreg(qapp, monkeypatch): + """Off Windows the system theme follows the lightness of the Qt palette.""" + from PySide6.QtGui import QColor, QPalette + from phyml_ui import themes + monkeypatch.setattr(themes, 'winreg', None) + saved = qapp.palette() + try: + dark = QPalette(saved) + dark.setColor(QPalette.Window, QColor('#202124')) + qapp.setPalette(dark) + assert themes.system_theme() == 'dark' + light = QPalette(saved) + light.setColor(QPalette.Window, QColor('#F5F5F5')) + qapp.setPalette(light) + assert themes.system_theme() == 'light' + finally: + qapp.setPalette(saved) diff --git a/tests/unit/fake_tool.py b/tests/unit/fake_tool.py index 69f705e..d6fcf21 100644 --- a/tests/unit/fake_tool.py +++ b/tests/unit/fake_tool.py @@ -27,7 +27,8 @@ def say(text): - sys.stdout.write(text + '\n') + # bytes, like real programs: a cp1252 pipe cannot encode a Cyrillic path + sys.stdout.buffer.write((text + '\n').encode('utf-8')) sys.stdout.flush() diff --git a/tests/unit/fakes.py b/tests/unit/fakes.py index cd0d819..46d3910 100644 --- a/tests/unit/fakes.py +++ b/tests/unit/fakes.py @@ -63,6 +63,13 @@ def launcher(self, params: dict) -> Native: def fake_muscle(tmp_path, mode: str) -> str: """A muscle-.cmd in tmp_path that runs fake_tool.py in that MUSCLE mode.""" + if os.name != 'nt': # a POSIX shell script on Linux + script = os.path.join(str(tmp_path), f'muscle-{mode}') + with open(script, 'w', encoding='utf-8', newline='\n') as handle: + handle.write('#!/bin/sh\n') + handle.write(f'exec "{sys.executable}" "{FAKE_TOOL}" {mode} "$@"\n') + os.chmod(script, 0o755) + return script script = os.path.join(str(tmp_path), f'muscle-{mode}.cmd') with open(script, 'w', encoding='oem', newline='\r\n') as handle: handle.write('@echo off\n') diff --git a/tests/unit/test_engine_iqtree.py b/tests/unit/test_engine_iqtree.py index c9710f1..bc2f053 100644 --- a/tests/unit/test_engine_iqtree.py +++ b/tests/unit/test_engine_iqtree.py @@ -3,6 +3,7 @@ import shutil import pytest +from phyml_core.utils import default_mpirun, exe_name from conftest import DATA, ROOT from phyml_core.engines import ENGINES @@ -27,7 +28,7 @@ def paths(tmp_path): @pytest.fixture def exe(tmp_path): """An existing IQ-TREE-looking file outside bin/.""" - path = tmp_path / 'iqtree2.exe' + path = tmp_path / exe_name('iqtree2') path.write_bytes(b'') return str(path) @@ -166,7 +167,7 @@ def test_executable_prefers_the_configured_file(paths, exe): def test_executable_falls_back_to_bin(paths): - bin_exe = os.path.join(paths.bin_dir, 'iqtree2.exe') + bin_exe = os.path.join(paths.bin_dir, exe_name('iqtree2')) os.makedirs(paths.bin_dir) open(bin_exe, 'wb').close() assert IqTree().executable({'iqtree_path': ''}, paths) == bin_exe @@ -208,7 +209,7 @@ def test_validate_empty_path_without_bin_reports_the_missing_exe(paths): def test_validate_uses_the_bin_fallback(paths): os.makedirs(paths.bin_dir) - open(os.path.join(paths.bin_dir, 'iqtree2.exe'), 'wb').close() + open(os.path.join(paths.bin_dir, exe_name('iqtree2')), 'wb').close() assert IqTree().validate(dict(Q, iqtree_path=''), paths) == [] diff --git a/tests/unit/test_engine_phyml.py b/tests/unit/test_engine_phyml.py index 0cd1f30..d9e5a67 100644 --- a/tests/unit/test_engine_phyml.py +++ b/tests/unit/test_engine_phyml.py @@ -3,6 +3,7 @@ import shutil import pytest +from phyml_core.utils import default_mpirun, exe_name from conftest import DATA, ROOT from phyml_core import settings @@ -30,7 +31,7 @@ def paths(tmp_path): @pytest.fixture def exe(tmp_path): """An existing PhyML-looking file outside bin/.""" - path = tmp_path / 'phyml.exe' + path = tmp_path / exe_name('phyml') path.write_bytes(b'') return str(path) @@ -232,25 +233,38 @@ def test_executable_prefers_the_configured_file(paths, exe): def test_executable_falls_back_to_bin(paths): - bin_exe = os.path.join(paths.bin_dir, 'phyml-mpi.exe') + bin_exe = os.path.join(paths.bin_dir, exe_name('phyml-mpi')) os.makedirs(paths.bin_dir) open(bin_exe, 'wb').close() assert PhyML().executable({'phyml_path': ''}, paths) == bin_exe assert PhyML().executable({}, paths) == bin_exe -def test_executable_prefers_the_mpi_build_in_bin(paths): +def test_executable_picks_the_build_matching_the_mpi_mode(paths): os.makedirs(paths.bin_dir) - for name in ('phyml.exe', 'phyml-mpi.exe'): + for name in (exe_name('phyml'), exe_name('phyml-mpi')): open(os.path.join(paths.bin_dir, name), 'wb').close() - assert PhyML().executable({}, paths) == os.path.join(paths.bin_dir, 'phyml-mpi.exe') + assert PhyML().executable({'use_mpi': True}, paths) == os.path.join(paths.bin_dir, exe_name('phyml-mpi')) + serial = os.path.join(paths.bin_dir, exe_name('phyml')) + assert PhyML().executable({'use_mpi': False}, paths) == serial + assert PhyML().executable({}, paths) == serial + + +def test_serial_bootstrap_is_valid_when_the_serial_build_is_bundled(paths): + """AppImage ships phyml and phyml-mpi: defaults (bootstrap, no MPI) must use phyml.""" + os.makedirs(paths.bin_dir) + for name in (exe_name('phyml'), exe_name('phyml-mpi')): + with open(os.path.join(paths.bin_dir, name), 'wb') as f: + f.write(b'MPI_Init' if name.startswith('phyml-mpi') else b'serial') + issues = PhyML().validate({'use_mpi': False, 'support_method': 'bootstrap'}, paths) + assert [i.key for i in issues] == [] def test_executable_resolves_a_stored_relative_path(paths): os.makedirs(os.path.join(paths.app_dir, 'tools')) - target = os.path.join(paths.app_dir, 'tools', 'phyml.exe') + target = os.path.join(paths.app_dir, 'tools', exe_name('phyml')) open(target, 'wb').close() - assert PhyML().executable({'phyml_path': os.path.join('tools', 'phyml.exe')}, paths) == target + assert PhyML().executable({'phyml_path': os.path.join('tools', exe_name('phyml'))}, paths) == target def test_executable_returns_the_configured_path_when_nothing_exists(paths, tmp_path): @@ -286,12 +300,12 @@ def test_validate_empty_path_without_bin_reports_the_missing_exe(paths): def test_validate_uses_the_bin_fallback(paths): os.makedirs(paths.bin_dir) - open(os.path.join(paths.bin_dir, 'phyml.exe'), 'wb').close() + open(os.path.join(paths.bin_dir, exe_name('phyml')), 'wb').close() assert PhyML().validate(dict(P, phyml_path=''), paths) == [] def test_validate_serial_bootstrap_on_mpi_build(paths, tmp_path): - mpi_exe = tmp_path / 'phyml-mpi.exe' + mpi_exe = tmp_path / exe_name('phyml-mpi') mpi_exe.write_bytes(b'') issues = PhyML().validate(dict(P, phyml_path=str(mpi_exe), use_mpi=False), paths) assert issues == [Issue('error', 'mpi_boot_msg')] @@ -299,24 +313,24 @@ def test_validate_serial_bootstrap_on_mpi_build(paths, tmp_path): def test_validate_mpi_build_is_fine_with_mpi_alrt_or_a_serial_build(paths, tmp_path, monkeypatch): monkeypatch.setattr(shutil, 'which', lambda *a, **k: 'C:/MS/mpiexec.exe') - mpi_exe = tmp_path / 'phyml-mpi.exe' + mpi_exe = tmp_path / exe_name('phyml-mpi') mpi_exe.write_bytes(b'') base = dict(P, phyml_path=str(mpi_exe), mpirun_path='mpiexec', mpi_np='2') assert PhyML().validate(dict(base, use_mpi=True), paths) == [] assert PhyML().validate(dict(base, use_mpi=False, support_method='-4'), paths) == [] - serial = tmp_path / 'phyml.exe' + serial = tmp_path / exe_name('phyml') serial.write_bytes(b'') assert PhyML().validate(dict(base, phyml_path=str(serial), use_mpi=False), paths) == [] def test_validate_serial_bootstrap_check_sees_the_bin_fallback(paths): os.makedirs(paths.bin_dir) - open(os.path.join(paths.bin_dir, 'phyml-mpi.exe'), 'wb').close() + open(os.path.join(paths.bin_dir, exe_name('phyml-mpi')), 'wb').close() assert keys(PhyML().validate(dict(P, phyml_path='', use_mpi=False), paths)) == ['mpi_boot_msg'] def test_validate_reports_only_the_missing_exe_not_a_guess_about_its_build(paths, tmp_path): - missing = str(tmp_path / 'phyml-mpi.exe') + missing = str(tmp_path / exe_name('phyml-mpi')) assert keys(PhyML().validate(dict(P, phyml_path=missing, use_mpi=False), paths)) == ['val_no_exe'] @@ -403,12 +417,13 @@ def test_validate_mpiexec_present_as_a_file_or_a_command_name(paths, exe, tmp_pa @pytest.mark.parametrize('blank', ['', ' ']) def test_blank_mpirun_path_falls_back_to_mpiexec(paths, exe, monkeypatch, blank): - monkeypatch.setattr(shutil, 'which', lambda name, *a, **k: 'C:/MS/mpiexec.exe' if name == 'mpiexec' else None) + launcher_name = default_mpirun() # mpiexec on Windows, mpirun elsewhere + monkeypatch.setattr(shutil, 'which', lambda name, *a, **k: '/x/' + name if name == launcher_name else None) params = dict(P, phyml_path=exe, use_mpi=True, mpirun_path=blank, mpi_np='3') assert PhyML().validate(params, paths) == [] - assert PhyML().launcher(params) == Mpi('C:/MS/mpiexec.exe', 3) + assert PhyML().launcher(params) == Mpi('/x/' + launcher_name, 3) monkeypatch.setattr(shutil, 'which', lambda *a, **k: None) - assert PhyML().validate(params, paths) == [Issue('error', 'val_no_mpiexec', ('mpiexec',))] + assert PhyML().validate(params, paths) == [Issue('error', 'val_no_mpiexec', (launcher_name,))] def test_validate_mpiexec_is_not_checked_when_mpi_is_off(paths, exe, monkeypatch): diff --git a/tests/unit/test_fetch_third_party.py b/tests/unit/test_fetch_third_party.py index 4383cb9..50736dc 100644 --- a/tests/unit/test_fetch_third_party.py +++ b/tests/unit/test_fetch_third_party.py @@ -50,3 +50,84 @@ def test_fetch_extracts_listed_members(tmp_path): assert [Path(p) for p in paths] == [dest / 'tool.exe'] assert sorted(p.name for p in dest.iterdir()) == ['tool.exe'] assert (dest / 'tool.exe').read_bytes() == b'exe' + + +def test_fetch_extracts_tar_gz_member(tmp_path): + import tarfile + payload = tmp_path / 'iqtree2' + payload.write_bytes(b'elf') + archive = tmp_path / 'iq.tar.gz' + with tarfile.open(archive, 'w:gz') as t: + t.add(payload, arcname='iqtree-2.4.0-Linux-intel/bin/iqtree2') + t.add(payload, arcname='iqtree-2.4.0-Linux-intel/bin/other') + dest = tmp_path / 'bin' + spec = {'url': archive.as_uri(), 'sha256': _sha(archive), + 'extract': {'iqtree-2.4.0-Linux-intel/bin/iqtree2': 'iqtree2'}} + paths = fetch_mod.fetch('iqtree', spec, str(dest)) + assert [Path(p) for p in paths] == [dest / 'iqtree2'] + assert sorted(p.name for p in dest.iterdir()) == ['iqtree2'] + + +def test_fetch_runs_build_script(tmp_path): + import sys + src = tmp_path / 'src.tar.gz' + src.write_bytes(b'sources') + builder = tmp_path / 'builder.py' + builder.write_text( + 'import sys, pathlib\n' + 'archive, dest = sys.argv[1], pathlib.Path(sys.argv[2])\n' + 'dest.mkdir(parents=True, exist_ok=True)\n' + "(dest / 'phyml').write_bytes(pathlib.Path(archive).read_bytes())\n", 'utf-8') + dest = tmp_path / 'bin' + spec = {'url': src.as_uri(), 'sha256': _sha(src), + 'build': [sys.executable, str(builder)], 'outputs': ['phyml']} + paths = fetch_mod.fetch('phyml', spec, str(dest)) + assert [Path(p) for p in paths] == [dest / 'phyml'] + assert (dest / 'phyml').read_bytes() == b'sources' + + +def test_fetch_build_missing_output_fails(tmp_path): + import sys + src = tmp_path / 'src.tar.gz' + src.write_bytes(b'sources') + builder = tmp_path / 'builder.py' + builder.write_text('pass\n', 'utf-8') + spec = {'url': src.as_uri(), 'sha256': _sha(src), + 'build': [sys.executable, str(builder)], 'outputs': ['phyml']} + with pytest.raises(ValueError): + fetch_mod.fetch('phyml', spec, str(tmp_path / 'bin')) + + +@pytest.mark.skipif(__import__('os').name == 'nt', reason='POSIX file modes') +def test_fetch_sets_mode_on_posix(tmp_path): + import os, stat + src = tmp_path / 'muscle-linux' + src.write_bytes(b'elf') + spec = {'url': src.as_uri(), 'sha256': _sha(src), 'save_as': 'muscle', 'mode': '755'} + fetch_mod.fetch('muscle', spec, str(tmp_path / 'bin')) + assert stat.S_IMODE(os.stat(tmp_path / 'bin' / 'muscle').st_mode) == 0o755 + + +def test_manifest_has_both_platforms_and_valid_hashes(): + import json, re + manifest = json.loads((ROOT / 'packaging' / 'third_party.json').read_text('utf-8')) + assert {'windows', 'linux'} <= set(manifest) + for section in ('windows', 'linux'): + assert manifest[section], section + for name, spec in manifest[section].items(): + assert re.fullmatch(r'[0-9a-f]{64}', spec['sha256']), (section, name) + assert spec['url'].startswith('https://'), (section, name) + assert sum(k in spec for k in ('extract', 'save_as', 'build')) == 1, (section, name) + + +def test_main_selects_platform_section(tmp_path, monkeypatch): + import json + src = tmp_path / 'tool' + src.write_bytes(b'x') + manifest = {'windows': {'a': {'url': src.as_uri(), 'sha256': _sha(src), 'save_as': 'a.exe'}}, + 'linux': {'b': {'url': src.as_uri(), 'sha256': _sha(src), 'save_as': 'b'}}} + mpath = tmp_path / 'm.json' + mpath.write_text(json.dumps(manifest), 'utf-8') + monkeypatch.setattr(fetch_mod, 'MANIFEST', str(mpath)) + assert fetch_mod.main(['--platform', 'linux', '--dest', str(tmp_path / 'out')]) == 0 + assert sorted(p.name for p in (tmp_path / 'out').iterdir()) == ['b'] diff --git a/tests/unit/test_package.py b/tests/unit/test_package.py new file mode 100644 index 0000000..94c2312 --- /dev/null +++ b/tests/unit/test_package.py @@ -0,0 +1,42 @@ +"""Release packaging helpers (packaging/package.py) and the --version flag.""" +import importlib.util +import subprocess +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +_spec = importlib.util.spec_from_file_location('package', ROOT / 'packaging' / 'package.py') +package = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(package) + + +@pytest.mark.parametrize('tag, version', [('v2.0.0', '2.0.0'), ('v2.0.0-rc1', '2.0.0-rc1'), + ('v10.11.12', '10.11.12')]) +def test_version_from_tag(tag, version): + assert package.version_from_tag(tag) == version + + +@pytest.mark.parametrize('tag', ['2.0.0', 'v2', 'v2.0', 'release-1', 'v2.0.0 ', 'v2.0.0-']) +def test_version_from_tag_rejects(tag): + with pytest.raises(ValueError): + package.version_from_tag(tag) + + +def test_stamp_version_does_not_touch_repo_file(tmp_path): + repo_init = ROOT / 'phyml_core' / '__init__.py' + before = repo_init.read_bytes() + copy = tmp_path / '__init__.py' + copy.write_bytes(before) + package.stamp_version('2.0.0-rc1', str(copy)) + assert '__version__ = "2.0.0-rc1"' in copy.read_text('utf-8') + assert repo_init.read_bytes() == before + + +def test_cli_version_flag(): + from phyml_core import __version__ + r = subprocess.run([sys.executable, str(ROOT / 'phymlgui.py'), '--version'], + capture_output=True, text=True, cwd=str(ROOT), timeout=60) + assert r.returncode == 0, r.stderr + assert r.stdout.strip() == f'phymlGUI {__version__}' diff --git a/tests/unit/test_paths.py b/tests/unit/test_paths.py index aaef52a..47acc41 100644 --- a/tests/unit/test_paths.py +++ b/tests/unit/test_paths.py @@ -35,7 +35,7 @@ def test_detect_prefers_writable_app_dir(tmp_path): def test_detect_falls_back_to_appdata(tmp_path): p = AppPaths.detect(env={'APPDATA': str(tmp_path / 'ad')}, is_writable=lambda d: False, - app_dir=str(tmp_path / 'app')) + app_dir=str(tmp_path / 'app'), platform='nt') assert p.data_dir == str(tmp_path / 'ad' / 'phymlGUI') and os.path.isdir(p.data_dir) @@ -48,7 +48,7 @@ def test_detect_default_probe_creates_and_removes_temp_file(tmp_path): def test_detect_default_probe_rejects_missing_dir(tmp_path): missing = tmp_path / 'no-such-app' - p = AppPaths.detect(env={'APPDATA': str(tmp_path / 'ad')}, app_dir=str(missing)) + p = AppPaths.detect(env={'APPDATA': str(tmp_path / 'ad')}, app_dir=str(missing), platform='nt') assert p.data_dir == str(tmp_path / 'ad' / 'phymlGUI') assert not missing.exists() diff --git a/tests/unit/test_platform.py b/tests/unit/test_platform.py new file mode 100644 index 0000000..f116056 --- /dev/null +++ b/tests/unit/test_platform.py @@ -0,0 +1,113 @@ +"""Linux portability of the core: tool names, PATH lookup, data dir, mpirun.""" +import json +import os +import shutil + +from phyml_core import settings, utils +from phyml_core.engines.base import Issue +from phyml_core.engines.phyml import PhyML +from phyml_core.paths import AppPaths + + +def test_exe_name_per_platform(monkeypatch): + monkeypatch.setattr(utils, 'IS_WINDOWS', True) + assert utils.exe_name('muscle') == 'muscle.exe' + assert utils.default_mpirun() == 'mpiexec' + monkeypatch.setattr(utils, 'IS_WINDOWS', False) + assert utils.exe_name('muscle') == 'muscle' + assert utils.default_mpirun() == 'mpirun' + + +def test_find_tool_falls_back_to_path(tmp_path, monkeypatch): + p = AppPaths(app_dir=str(tmp_path / 'app'), data_dir=str(tmp_path)) + on_path = str(tmp_path / 'usr' / 'muscle') + monkeypatch.setattr(shutil, 'which', lambda name, *a, **k: on_path if name == 'muscle' else None) + assert p.find_tool('', ('muscle',)) == on_path + # bin/ still wins over PATH + os.makedirs(p.bin_dir) + bundled = os.path.join(p.bin_dir, 'muscle') + open(bundled, 'w').close() + assert p.find_tool('', ('muscle',)) == bundled + + +def test_detect_falls_back_to_xdg_on_linux(tmp_path): + xdg = tmp_path / 'xdg' + p = AppPaths.detect(env={'XDG_CONFIG_HOME': str(xdg)}, is_writable=lambda d: False, + app_dir=str(tmp_path / 'app'), platform='posix') + assert p.data_dir == str(xdg / 'phymlGUI') and os.path.isdir(p.data_dir) + home = tmp_path / 'home' + p = AppPaths.detect(env={'HOME': str(home)}, is_writable=lambda d: False, + app_dir=str(tmp_path / 'app'), platform='posix') + assert p.data_dir == str(home / '.config' / 'phymlGUI') + + +def test_validate_mpirun_missing_on_linux(tmp_path, monkeypatch): + monkeypatch.setattr(utils, 'IS_WINDOWS', False) + monkeypatch.setattr(shutil, 'which', lambda *a, **k: None) + exe = tmp_path / 'phyml-mpi' + exe.write_text('') + paths = AppPaths(app_dir=str(tmp_path), data_dir=str(tmp_path)) + params = dict(settings.DEFAULTS, phyml_path=str(exe), use_mpi=True, mpi_np='2', + mpirun_path='mpirun', support_method='bootstrap') + assert PhyML().validate(params, paths) == [Issue('error', 'val_no_mpiexec', ('mpirun',))] + # an empty field falls back to the platform default, mpirun + assert PhyML().validate(dict(params, mpirun_path=''), paths) == [ + Issue('error', 'val_no_mpiexec', ('mpirun',))] + + +def test_windows_config_loads_on_linux(tmp_path, monkeypatch): + monkeypatch.setattr(utils, 'IS_WINDOWS', False) + monkeypatch.setattr(shutil, 'which', lambda *a, **k: None) + cfg_file = tmp_path / 'phyml_config.json' + cfg_file.write_text(json.dumps({'mpirun_path': 'mpiexec', + 'phyml_path': 'C:\\x\\phyml-mpi.exe'}), 'utf-8') + paths = AppPaths(app_dir=str(tmp_path / 'app'), data_dir=str(tmp_path)) + cfg = settings.from_disk(settings.load(str(cfg_file)), paths) + assert cfg['mpirun_path'] == 'mpirun' + issues = PhyML().validate(dict(cfg, mpi_np='2'), paths) + assert issues and issues[0].key == 'val_no_exe' + + +def test_detect_never_uses_app_dir_inside_an_appimage(tmp_path): + """--appimage-extract-and-run unpacks into a writable temp folder; settings must + still go to ~/.config/phymlGUI, not into that throwaway folder.""" + p = AppPaths.detect(env={'APPIMAGE': '/home/u/phymlGUI.AppImage', 'HOME': str(tmp_path)}, + is_writable=lambda d: True, app_dir=str(tmp_path / 'squashfs-root' / 'usr' / 'bin'), + platform='posix') + assert p.data_dir == str(tmp_path / '.config' / 'phymlGUI') + + +def test_child_env_undoes_the_pyinstaller_library_path(monkeypatch): + """The frozen Linux bootloader puts _internal first on LD_LIBRARY_PATH; mpirun, + phyml-mpi and xdg-open must get the user's own value back (or none).""" + monkeypatch.setattr(utils, 'IS_WINDOWS', False) + frozen = {'LD_LIBRARY_PATH': '/tmp/_internal:/opt/lib', 'LD_LIBRARY_PATH_ORIG': '/opt/lib', 'X': '1'} + assert utils.child_env(frozen, frozen=True) == {'LD_LIBRARY_PATH': '/opt/lib', 'X': '1'} + assert utils.child_env({'LD_LIBRARY_PATH': '/tmp/_internal', 'X': '1'}, frozen=True) == {'X': '1'} + plain = {'LD_LIBRARY_PATH': '/opt/lib', 'X': '1'} + assert utils.child_env(plain, frozen=False) == plain + monkeypatch.setattr(utils, 'IS_WINDOWS', True) + assert utils.child_env(frozen, frozen=True) == frozen + + +def test_runner_starts_programs_with_the_child_env(monkeypatch): + import subprocess + from phyml_core import runner + seen = {} + + class Stop(Exception): + pass + + def fake_popen(argv, **kwargs): + seen.update(kwargs) + raise Stop + monkeypatch.setattr(utils, 'child_env', lambda: {'MARK': 'child'}) + monkeypatch.setattr(subprocess, 'Popen', fake_popen) + r = runner.PipelineRunner.__new__(runner.PipelineRunner) + import threading + r._lock, r._cancelled = threading.Lock(), threading.Event() + try: + r._execute(['x'], 'cancel', False) + except Stop: + pass + assert seen.get('env') == {'MARK': 'child'} diff --git a/tests/unit/test_raxml_parity.py b/tests/unit/test_raxml_parity.py new file mode 100644 index 0000000..d25f140 --- /dev/null +++ b/tests/unit/test_raxml_parity.py @@ -0,0 +1,54 @@ +"""RAxML-NG Windows vs Linux parity check (packaging/raxml_parity.py).""" +import importlib.util +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +_spec = importlib.util.spec_from_file_location('raxml_parity', ROOT / 'packaging' / 'raxml_parity.py') +parity = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(parity) + +TREE = '((A:0.1,B:0.2):0.05,(C:0.1,D:0.3):0.05,E:0.2);\n' +SWAPPED = '((A:0.1,C:0.2):0.05,(B:0.1,D:0.3):0.05,E:0.2);\n' + + +def _outputs(folder, best=TREE, boots=(TREE, SWAPPED), lnl='-1823.506511', fixed='-1900.123456'): + folder.mkdir(parents=True) + (folder / 'search.raxml.bestTree').write_text(best) + (folder / 'search.raxml.log').write_text(f'... Final LogLikelihood: {lnl}\n') + (folder / 'boot.raxml.bootstraps').write_text(''.join(boots)) + (folder / 'eval.raxml.log').write_text(f'Final LogLikelihood: {fixed}\n') + return str(folder) + + +def test_identical_outputs_match(tmp_path): + assert parity.compare(_outputs(tmp_path / 'w'), _outputs(tmp_path / 'l')) == [] + + +def test_crlf_and_tiny_lnl_difference_still_match(tmp_path): + win = _outputs(tmp_path / 'w', best=TREE.replace('\n', '\r\n'), lnl='-1823.5066') + assert parity.compare(win, _outputs(tmp_path / 'l')) == [] + + +def test_different_best_tree_is_reported(tmp_path): + problems = parity.compare(_outputs(tmp_path / 'w', best=SWAPPED), _outputs(tmp_path / 'l')) + assert len(problems) == 1 and 'search.raxml.bestTree' in problems[0] + + +def test_lnl_difference_is_reported(tmp_path): + problems = parity.compare(_outputs(tmp_path / 'w', lnl='-1823.40'), _outputs(tmp_path / 'l')) + assert len(problems) == 1 and 'search.raxml.log' in problems[0] + + +def test_bootstrap_and_fixed_tree_differences_are_reported(tmp_path): + win = _outputs(tmp_path / 'w', boots=(TREE, TREE), fixed='-1900.123999') + problems = parity.compare(win, _outputs(tmp_path / 'l')) + assert len(problems) == 2 + assert any('bootstraps' in p for p in problems) and any('eval.raxml.log' in p for p in problems) + + +def test_missing_file_is_reported(tmp_path): + lin = _outputs(tmp_path / 'l') + win = _outputs(tmp_path / 'w') + (Path(win) / 'boot.raxml.bootstraps').unlink() + problems = parity.compare(win, lin) + assert problems and 'boot.raxml.bootstraps' in problems[0] diff --git a/tests/unit/test_release_meta.py b/tests/unit/test_release_meta.py new file mode 100644 index 0000000..167770a --- /dev/null +++ b/tests/unit/test_release_meta.py @@ -0,0 +1,82 @@ +"""Release metadata helpers (packaging/release_meta.py).""" +import hashlib +import importlib.util +import json +import tarfile +import zipfile +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +_spec = importlib.util.spec_from_file_location('release_meta', ROOT / 'packaging' / 'release_meta.py') +meta = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(meta) + + +@pytest.mark.parametrize('ref, version', [('refs/tags/v2.0.0', '2.0.0'), + ('refs/tags/v2.0.0-rc1', '2.0.0-rc1'), + ('refs/heads/release', '0.0.0-dev')]) +def test_release_version_from_ref(ref, version): + assert meta.version_for_ref(ref) == version + + +@pytest.mark.parametrize('ref', ['refs/tags/release-1', 'refs/tags/v2', 'refs/tags/2.0.0']) +def test_release_tag_validation(ref): + with pytest.raises(ValueError): + meta.version_for_ref(ref) + + +def test_sha256sums_format(tmp_path): + a, b = tmp_path / 'a.zip', tmp_path / 'b.AppImage' + a.write_bytes(b'one') + b.write_bytes(b'two') + out = tmp_path / 'SHA256SUMS' + meta.write_sha256sums([str(a), str(b)], str(out)) + lines = out.read_text('utf-8').splitlines() + assert lines == [f'{hashlib.sha256(b"one").hexdigest()} a.zip', + f'{hashlib.sha256(b"two").hexdigest()} b.AppImage'] + + +def test_release_notes_lists_tools(): + manifest = json.loads((ROOT / 'packaging' / 'third_party.json').read_text('utf-8')) + notes = meta.release_notes('2.0.0', manifest) + assert 'phymlGUI 2.0.0' in notes + for section in ('windows', 'linux'): + for name, spec in manifest[section].items(): + assert f" {spec['version'].split(' (')[0]} |" in notes, (section, name) + assert 'Microsoft MPI' in notes and 'OpenMPI' in notes and 'SHA256SUMS' in notes + + +def test_raxml_source_bundle(tmp_path): + src = tmp_path / 'raxml-ng_v2.0.3_source.zip' + with zipfile.ZipFile(src, 'w') as z: + z.writestr('CMakeLists.txt', 'project(raxml)') + patch = tmp_path / 'windows.patch' + patch.write_text('--- a\n+++ b\n') + out = tmp_path / 'raxml-ng-2.0.3-source+patch.tar.gz' + meta.raxml_source_bundle(str(src), str(patch), str(out)) + with tarfile.open(out) as t: + assert sorted(t.getnames()) == ['raxml-ng-2.0.3-source+patch/raxml-ng-2.0.3-windows.patch', + 'raxml-ng-2.0.3-source+patch/raxml-ng_v2.0.3_source.zip'] + + +def test_release_notes_are_bilingual(): + manifest = json.loads((ROOT / 'packaging' / 'third_party.json').read_text('utf-8')) + notes = meta.release_notes('2.0.0', manifest) + for text in ('Download', 'Скачать', 'Microsoft MPI', 'OpenMPI', 'SHA256SUMS'): + assert text in notes, text + assert notes.index('Download') < notes.index('Скачать') + + +def test_release_notes_hold_the_version_highlights_in_both_languages(tmp_path): + manifest = json.loads((ROOT / 'packaging' / 'third_party.json').read_text('utf-8')) + (tmp_path / '2.0.0.md').write_text('## Highlights\n\nNew window.\n', 'utf-8') + (tmp_path / '2.0.0.ru.md').write_text('## Главное\n\nНовое окно.\n', 'utf-8') + for version in ('2.0.0', '2.0.0-rc1'): # a release candidate uses the notes of its release + notes = meta.release_notes(version, manifest, notes_dir=str(tmp_path)) + assert notes.startswith(f'# phymlGUI {version}\n\n## Highlights\n\nNew window.\n'), version + order = [notes.index(t) for t in ('Highlights', 'Download', 'Главное', 'Скачать')] + assert order == sorted(order), version + plain = meta.release_notes('2.1.0', manifest, notes_dir=str(tmp_path)) + assert 'Highlights' not in plain and 'Главное' not in plain and 'Скачать' in plain diff --git a/tests/unit/test_release_scripts_without_pytest.py b/tests/unit/test_release_scripts_without_pytest.py new file mode 100644 index 0000000..2a8671e --- /dev/null +++ b/tests/unit/test_release_scripts_without_pytest.py @@ -0,0 +1,44 @@ +"""Release jobs install only requirements.txt: their scripts must not need pytest.""" +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] + +PRELUDE = f""" +import importlib.util, sys +sys.modules['pytest'] = None # import pytest -> ImportError, as on a release runner +def load(name): + spec = importlib.util.spec_from_file_location(name, {str(ROOT / 'packaging')!r} + '/' + name + '.py') + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module +""" + + +def _run(code, cwd): + return subprocess.run([sys.executable, '-c', PRELUDE + code], cwd=cwd, + capture_output=True, text=True, timeout=120) + + +def test_parity_inputs_are_generated_without_pytest(tmp_path): + r = _run(""" +try: + load('raxml_parity').run('no-such-raxml-ng', 'out') +except FileNotFoundError: + pass +""", tmp_path) + assert r.returncode == 0, r.stderr + assert (tmp_path / 'out' / 'parity.phy').is_file() + + +def test_smoke_dataset_is_generated_without_pytest(tmp_path): + r = _run(""" +import os +try: + load('smoke_test')._analysis(['no-such-phymlgui'], dict(os.environ), '.', 'phyml', {}) +except FileNotFoundError: + pass +""", tmp_path) + assert r.returncode == 0, r.stderr + assert (tmp_path / 'phyml' / 'unaligned.fasta').is_file() diff --git a/tests/unit/test_runner.py b/tests/unit/test_runner.py index b04ff38..bbd65d7 100644 --- a/tests/unit/test_runner.py +++ b/tests/unit/test_runner.py @@ -307,7 +307,9 @@ def test_undecodable_engine_output_is_replaced(tmp_path, paths): assert any(text.startswith('progress ') for text in rec.texts('out')) -def test_run_in_cyrillic_dir_with_spaces(tmp_path, paths): +def test_run_in_cyrillic_dir_with_spaces(tmp_path, paths, monkeypatch): + # an English Windows (CI) pipes child output as cp1252, which has no Cyrillic + monkeypatch.setenv('PYTHONIOENCODING', 'cp1252') folder = tmp_path / 'Мои данные' / 'проект 1' aligned = write_fasta(folder / 'выборка 1.fasta', ALIGNED) unaligned = write_fasta(folder / 'выборка 2.fasta', UNALIGNED)