Skip to content

Commit 968a486

Browse files
committed
ci(AppImage): Build a linux AppImage
1 parent 20c3090 commit 968a486

7 files changed

Lines changed: 220 additions & 8 deletions

File tree

.github/workflows/build_windows_macos.yml

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ permissions:
1616
contents: read
1717

1818
jobs:
19-
build:
19+
build_windows:
2020
runs-on: 'windows-latest'
2121
strategy:
2222
matrix:
@@ -246,23 +246,116 @@ jobs:
246246
path: macos/Output/*.dmg
247247
retention-days: 7
248248

249+
build_linux:
250+
runs-on: 'ubuntu-20.04' # Use a consistent Linux environment for building the AppImage
251+
strategy:
252+
matrix:
253+
python-version: ['3.14']
254+
255+
permissions:
256+
contents: write
257+
id-token: write
258+
259+
steps:
260+
- name: Harden the runner (Audit all outbound calls)
261+
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
262+
with:
263+
egress-policy: audit
264+
265+
- name: Checkout
266+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
267+
268+
- name: Install system dependencies for tkinter
269+
run: |
270+
sudo apt-get update -qq
271+
sudo apt-get install -y --no-install-recommends \
272+
python3-tk \
273+
tk-dev \
274+
tcl-dev \
275+
squashfs-tools
276+
277+
- name: Set up Python
278+
id: setup-python
279+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
280+
with:
281+
python-version: ${{ matrix.python-version }}
282+
283+
- name: Install dependencies
284+
run: |
285+
python -m pip install --upgrade pip
286+
python -m pip install .[linux_dist]
287+
288+
- name: Verify tkinter is available
289+
run: |
290+
python - <<'PY'
291+
import tkinter
292+
print(f"tkinter OK, TkVersion={tkinter.TkVersion}")
293+
PY
294+
295+
- name: Write the git commit hash to file
296+
run: |
297+
git rev-parse HEAD | tr -d '\n' > git_hash.txt
298+
299+
- name: Build Linux app with PyInstaller
300+
run: |
301+
pyinstaller --clean linux/ardupilot_methodic_configurator.spec
302+
303+
- name: Build AppImage
304+
run: |
305+
set -euo pipefail
306+
VERSION=$(python -c "import ardupilot_methodic_configurator; print(ardupilot_methodic_configurator.__version__)")
307+
APP_NAME="ardupilot_methodic_configurator"
308+
APPIMAGE_NAME="${APP_NAME}_${VERSION}_linux_setup.AppImage"
309+
APPDIR="linux/AppDir"
310+
OUTPUT_DIR="linux/Output"
311+
312+
# Create AppDir structure
313+
rm -rf "$APPDIR"
314+
mkdir -p "$APPDIR" "$OUTPUT_DIR"
315+
316+
# Copy PyInstaller output
317+
cp -R "dist/${APP_NAME}" "$APPDIR/"
318+
319+
# Copy desktop file and icon
320+
cp "linux/${APP_NAME}.desktop" "$APPDIR/"
321+
cp "ardupilot_methodic_configurator/images/ArduPilot_icon.png" "$APPDIR/${APP_NAME}.png"
322+
323+
# Copy AppRun and make executable
324+
cp "linux/AppRun" "$APPDIR/AppRun"
325+
chmod +x "$APPDIR/AppRun"
326+
327+
# Download appimagetool (use --appimage-extract-and-run since FUSE is unavailable in CI)
328+
# Pinned to 1.9.1 for security and reproducibility
329+
wget -q "https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage" \
330+
-O appimagetool
331+
chmod +x appimagetool
332+
333+
ARCH=x86_64 ./appimagetool --appimage-extract-and-run "$APPDIR" "$OUTPUT_DIR/$APPIMAGE_NAME"
334+
335+
- name: Upload Linux build artifacts
336+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
337+
with:
338+
name: linux-appimage
339+
path: linux/Output/*.AppImage
340+
retention-days: 7
341+
249342
# Generate SLSA provenance using the official generic workflow
250343
# provenance:
251-
# needs: [build]
344+
# needs: [build_windows]
252345
# permissions:
253346
# actions: read # To read the workflow path
254347
# id-token: write # To sign the provenance
255348
# contents: write # To add assets to a release
256349
# uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@4876e96b8268fd8b7b8d8574718d06c0d0426d40 # latest commit
257350
# with:
258-
# base64-subjects: "${{ needs.build.outputs.hashes }}"
351+
# base64-subjects: "${{ needs.build_windows.outputs.hashes }}"
259352
# upload-assets: ${{ startsWith(github.ref, 'refs/tags/v') }} # Only upload to releases for v* tags
260353
# continue-on-error: false # Explicit error handling - fail fast for security issues
261354

262355
# Release job that depends on provenance generation
263356
release:
264-
# needs: [build, provenance]
265-
needs: [build, build_macos]
357+
# needs: [build_windows, provenance]
358+
needs: [build_windows, build_macos, build_linux]
266359
runs-on: windows-latest
267360
if: startsWith(github.ref, 'refs/tags/v') || github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
268361
permissions:
@@ -277,7 +370,7 @@ jobs:
277370
- name: Checkout
278371
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
279372

280-
- name: Download build artifacts
373+
- name: Download Windows build artifacts
281374
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
282375
with:
283376
name: windows-installer
@@ -289,16 +382,24 @@ jobs:
289382
name: macos-dmg
290383
path: release-artifacts
291384

385+
- name: Download Linux build artifacts
386+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
387+
with:
388+
name: linux-appimage
389+
path: release-artifacts
390+
292391
- name: Rename installer for pre-release
293392
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
294393
shell: pwsh
295394
run: |
296-
$files = Get-ChildItem release-artifacts\*.* | Where-Object { $_.Extension -in '.exe', '.dmg' }
395+
$files = Get-ChildItem release-artifacts\*.* | Where-Object { $_.Extension -in '.exe', '.dmg', '.AppImage' }
297396
foreach ($file in $files) {
298397
if ($file.Extension -eq '.exe') {
299398
$newName = $file.Name -replace 'ardupilot_methodic_configurator_.*_windows_setup\.exe$', 'ardupilot_methodic_configurator_latest_pre_release_windows_setup.exe'
300399
} elseif ($file.Extension -eq '.dmg') {
301400
$newName = $file.Name -replace 'ardupilot_methodic_configurator_.*_macos_setup\.dmg$', 'ardupilot_methodic_configurator_latest_pre_release_macos_setup.dmg'
401+
} elseif ($file.Extension -eq '.AppImage') {
402+
$newName = $file.Name -replace 'ardupilot_methodic_configurator_.*_linux_setup\.AppImage$', 'ardupilot_methodic_configurator_latest_pre_release_linux_setup.AppImage'
302403
} else {
303404
continue
304405
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
| Lint | Quality | Test | Security | Deploy | Maintain |
44
| ---- | ------- | ---- | -------- | ------ | -------- |
55
| [![Pylint](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/pylint.yml/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/pylint.yml) | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/720794ed54014c58b9eaf7a097a4e98e)](https://app.codacy.com/gh/amilcarlucas/MethodicConfigurator/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) | [![pytest status](https://gist.githubusercontent.com/amilcarlucas/81b511dc0ff92b8072613d1cd100832e/raw/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/pytest.yml) | [![Known Vulnerabilities](https://snyk.io/test/github/amilcarlucas/MethodicConfigurator/badge.svg)](https://app.snyk.io/org/amilcarlucas/project/c8fd6e29-715b-4949-b828-64eff84f5fe1) | [![pages-build-deployment](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/pages/pages-build-deployment/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/pages/pages-build-deployment) | [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/ArduPilot/MethodicConfigurator.svg)](http://isitmaintained.com/project/ArduPilot/MethodicConfigurator) |
6-
| [![test Python cleanliness](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/ruff.yml/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/ruff.yml) | [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9101/badge)](https://www.bestpractices.dev/projects/9101) | [![Pytest tests](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/pytest.yml/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/pytest.yml) | [![CodeQL](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/github-code-scanning/codeql) | [![Upload MethodicConfigurator Package](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/python-publish.yml/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/python-publish.yml) | [![Percentage of issues still open](http://isitmaintained.com/badge/open/ArduPilot/MethodicConfigurator.svg)](http://isitmaintained.com/project/ArduPilot/MethodicConfigurator) |
6+
| [![test Python cleanliness](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/ruff.yml/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/ruff.yml) | [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9101/badge)](https://www.bestpractices.dev/projects/9101) | [![Pytest tests](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/pytest.yml/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/pytest.yml) | [![CodeQL](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/codeql.yml/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/codeql.yml) | [![Upload MethodicConfigurator Package](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/python-publish.yml/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/python-publish.yml) | [![Percentage of issues still open](http://isitmaintained.com/badge/open/ArduPilot/MethodicConfigurator.svg)](http://isitmaintained.com/project/ArduPilot/MethodicConfigurator) |
77
| [![mypy](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/mypy.yml/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/mypy.yml) | [![REUSE status](https://api.reuse.software/badge/github.com/ArduPilot/MethodicConfigurator)](https://api.reuse.software/info/github.com/ArduPilot/MethodicConfigurator) | [![Coverage Status](https://coveralls.io/repos/github/ArduPilot/MethodicConfigurator/badge.svg?branch=master)](https://coveralls.io/github/ArduPilot/MethodicConfigurator?branch=master) | [![gitavscan](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/gitavscan.yml/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/gitavscan.yml) | [![pypi](https://img.shields.io/pypi/v/ardupilot-methodic-configurator.svg)](https://pypi.org/project/ardupilot-methodic-configurator/) | [![python versions](https://img.shields.io/pypi/pyversions/ardupilot-methodic-configurator.svg)](https://pypi.python.org/pypi/ardupilot-methodic-configurator) |
88
| [![Pyright](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/pyright.yml/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/pyright.yml) | [![md-link-check](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/markdown-link-check.yml/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/markdown-link-check.yml) | [![Coverity Scan Build Status](https://scan.coverity.com/projects/30346/badge.svg)](https://scan.coverity.com/projects/ardupilot-methodic-configurator) | [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/ArduPilot/MethodicConfigurator/badge)](https://scorecard.dev/viewer/?uri=github.com/ArduPilot/MethodicConfigurator) | [![PyPI - Downloads](https://img.shields.io/pypi/dm/ardupilot-methodic-configurator?link=https%3A%2F%2Fpypi.org%2Fproject%2Fardupilot-methodic-configurator%2F)](https://pypistats.org/packages/ardupilot-methodic-configurator) | [![Code Climate](https://codeclimate.com/github/amilcarlucas/MethodicConfigurator.png)](https://codeclimate.com/github/amilcarlucas/MethodicConfigurator) |
99
| [![markdown-lint](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/markdown-lint.yml/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/markdown-lint.yml) | [![pre-commit](https://results.pre-commit.ci/badge/github/ArduPilot/MethodicConfigurator/master.svg)](https://results.pre-commit.ci/latest/github/ArduPilot/MethodicConfigurator/master) | [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.com/channels/674039678562861068/1308233496535371856) | | [![Windows Build](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/build_windows_macos.yml/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/windows_build.yml) | [![Update Flight Controller IDs](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/update_flightcontroller_ids.yml/badge.svg)](https://github.com/ArduPilot/MethodicConfigurator/actions/workflows/update_flightcontroller_ids.yml) |

REUSE.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@ path = ["windows/ardupilot_methodic_configurator*", "windows/settings_template.j
181181
SPDX-FileCopyrightText = "2024-2026 Amilcar Lucas"
182182
SPDX-License-Identifier = "GPL-3.0-or-later"
183183

184+
[[annotations]]
185+
path = ["linux/ardupilot_methodic_configurator*", "linux/AppRun"]
186+
SPDX-FileCopyrightText = "2024-2026 Amilcar Lucas"
187+
SPDX-License-Identifier = "GPL-3.0-or-later"
188+
184189
[[annotations]]
185190
path = [".dockerignore", "docker-compose.yml", "Dockerfile"]
186191
SPDX-FileCopyrightText = "2026 ArduPilot methodic configurator developers"

linux/AppRun

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/bash
2+
# AppRun entry point for the ArduPilot Methodic Configurator AppImage
3+
#
4+
# SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
5+
#
6+
# SPDX-License-Identifier: GPL-3.0-or-later
7+
8+
SELF=$(readlink -f "$0")
9+
HERE="${SELF%/*}"
10+
export PATH="${HERE}/ardupilot_methodic_configurator:${PATH}"
11+
exec "${HERE}/ardupilot_methodic_configurator/ardupilot_methodic_configurator" "$@"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[Desktop Entry]
2+
Name=ArduPilot Methodic Configurator
3+
Exec=ardupilot_methodic_configurator
4+
Icon=ardupilot_methodic_configurator
5+
Type=Application
6+
Categories=Utility;Science;
7+
Comment=Configure ArduPilot flight controller parameters in a methodical, traceable way
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# -*- mode: python -*-
2+
# spec file for pyinstaller to build ardupilot_methodic_configurator for Linux
3+
#
4+
# SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
5+
#
6+
# SPDX-License-Identifier: GPL-3.0-or-later
7+
8+
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
9+
import certifi
10+
import os
11+
12+
spec_file = globals().get("__file__")
13+
if spec_file:
14+
spec_dir = os.path.abspath(os.path.dirname(spec_file))
15+
else:
16+
spec_dir = os.path.abspath(os.getcwd())
17+
18+
19+
def _find_project_root(base_dir: str) -> str:
20+
candidates = [base_dir, os.path.abspath(os.path.join(base_dir, ".."))]
21+
for candidate in candidates:
22+
package_init = os.path.join(candidate, "ardupilot_methodic_configurator", "__init__.py")
23+
if os.path.exists(package_init):
24+
return candidate
25+
return base_dir
26+
27+
28+
PROJECT_ROOT = _find_project_root(spec_dir)
29+
PACKAGE_ROOT = os.path.join(PROJECT_ROOT, "ardupilot_methodic_configurator")
30+
31+
32+
certifi_cacert = certifi.where()
33+
34+
datas = [(certifi_cacert, "certifi")]
35+
36+
git_hash_path = os.path.join(PROJECT_ROOT, "git_hash.txt")
37+
if os.path.exists(git_hash_path):
38+
datas.append((git_hash_path, "ardupilot_methodic_configurator"))
39+
40+
datas += collect_data_files("ardupilot_methodic_configurator")
41+
42+
hidden_imports = (
43+
[
44+
"packaging",
45+
"packaging.version",
46+
"packaging.specifiers",
47+
]
48+
+ collect_submodules("ardupilot_methodic_configurator.modules")
49+
+ collect_submodules("pymavlink")
50+
)
51+
52+
analysis = Analysis(
53+
[os.path.join(PACKAGE_ROOT, "__main__.py")],
54+
pathex=[PROJECT_ROOT, PACKAGE_ROOT],
55+
hiddenimports=hidden_imports,
56+
datas=datas,
57+
hookspath=None,
58+
runtime_hooks=None,
59+
)
60+
61+
pyz = PYZ(analysis.pure)
62+
63+
exe = EXE(
64+
pyz,
65+
analysis.scripts,
66+
exclude_binaries=True,
67+
name="ardupilot_methodic_configurator",
68+
debug=False,
69+
strip=False,
70+
upx=False,
71+
console=False,
72+
)
73+
74+
coll = COLLECT(
75+
exe,
76+
analysis.binaries,
77+
analysis.zipfiles,
78+
analysis.datas,
79+
strip=False,
80+
upx=False,
81+
name="ardupilot_methodic_configurator",
82+
)

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ mac_dist = [
114114
"packaging==26.2",
115115
]
116116

117+
linux_dist = [
118+
"pip==26.0.1",
119+
"pyinstaller==6.20.0",
120+
"packaging==26.2",
121+
]
122+
117123
[project.scripts]
118124
ardupilot_methodic_configurator = "ardupilot_methodic_configurator.__main__:main"
119125
extract_param_defaults = "ardupilot_methodic_configurator.extract_param_defaults:main"

0 commit comments

Comments
 (0)