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
+
+
+## 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.
+
+
+
+### 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.
+
+
+
+### 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 со всеми программами анализа внутри. Русский и английский интерфейс, тёмная и светлая темы.
+
+
+
+## Возможности
+
+### 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.
+
+
+
+### 3. IQ-TREE с ModelFinder
+- **Подбор модели:** `MFP` поручает выбор модели замен ModelFinder; выбранная модель, лог-правдоподобие, параметр гаммы и время работы показываются на вкладке «Статистика».
+- **Ультрабыстрый бутстреп:** поддержки UFBoot на дереве; меньше 1000 реплик (минимум UFBoot) поднимается до 1000 с предупреждением.
+
+
+
+### 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)