Portability: technical reference¶
This page is the technical background for the roadmap: which code is portable, how each platform will work, what can go wrong, and how it is tested. The roadmap says what is built and when; this page says how and why.
It was written on 2026-10-13 from the code of version 0.2.1. Statements marked To verify were not checked on the platform itself; check them before relying on them.
1. Code map¶
About 9,650 lines of Python: 7,682 in deucalion/*.py and deucalion/gui/*.py, and 1,966 in
deucalion/gui/pages/. There is no platform check anywhere in the code today (no sys.platform
or os.name).
1.1 Portable as written¶
| Module | Notes |
|---|---|
manifest.py |
SQLite. Paths are stored as bytes with / as separator, so they are independent of the platform. |
names.py |
exFAT-safe names. Already handles Windows reserved names (CON, LPT1, …), trailing dots and spaces, and the 255 UTF-16 unit limit. fold_exfat is only .upper() (see §4.4). |
verify.py |
Reads the manifest and the drive. Imports backup, which isn't portable yet (§1.2). |
config.py |
Excluder and Config are portable. config_dir and state_dir use XDG paths. |
logs.py |
Portable. |
backup.Scanner, backup.Executor, restore.Restorer |
The planning and copying logic is portable, except the calls in §1.2. |
1.2 POSIX or Linux calls inside the engine¶
| Where | Call | Problem elsewhere |
|---|---|---|
backup.py (module top) |
import grp, pwd |
Doesn't exist on Windows: the engine can't be imported there. |
target.py (module top) |
import fcntl (used by Target.lock) |
Same, on Windows. |
recipe.py → helper.py (module top) |
import grp, pwd |
Same, on Windows. cli.py imports all of these, so deucalion --help fails on Windows. |
backup.Scanner.__init__ |
os.statvfs |
Missing on Windows; the AttributeError isn't caught by the surrounding except OSError. |
backup.Executor.run |
os.sync() |
Missing on Windows. |
backup.BackupJob._write_summary |
os.uname() |
Missing on Windows. |
backup._uname, backup._gname |
pwd.getpwuid, grp.getgrgid |
Missing on Windows. |
restore.Restorer.__init__, cli.main |
os.geteuid() |
Missing on Windows. |
backup._same_content |
st_ctime as change time |
On Windows it is the creation time (deprecated there since Python 3.12). The check for "edited, then its date put back" would quietly stop working. |
target.Target.create → util.is_on_system_disk, util.is_posix_fs |
findmnt, lsblk through util.run |
util.run doesn't catch a missing program: registering a drive crashes on macOS. |
target.Target.candidate_roots |
reads /proc/self/mountinfo |
Linux only. It fails quietly elsewhere, so a drive mounted at another place isn't found. |
restore.Restorer.execute.move_aside |
undo_root / str(path).lstrip("/") |
On Windows, str(path) starts with C:\, and joining an absolute path gives back the original path itself (§5.2). |
1.3 Linux-specific (works on any modern distribution)¶
| Module and function | Commands and interfaces |
|---|---|
udisks.UDisks |
UDisks2 over D-Bus (PyGObject), udisksctl loop-setup, udevadm settle, losetup -j |
drives.list_drives |
lsblk -J -b |
util.mount_info, mount_points, backing_disks, is_encrypted |
findmnt -J, lsblk -s -J, /sys/block/*/loop/backing_file |
drivesetup.DriveSetup |
the above, plus /dev/disk/by-partuuid, /dev/disk/by-uuid, /media/ and /run/media/ mount roots, blkid (in cli.py) |
privileged.Privileged |
pkexec, sudo, sudo -n, /usr/bin/python3 -I helper.py serve |
helper.op_veracrypt_create, op_veracrypt_open, op_veracrypt_close |
veracrypt --text --create, cryptsetup open --type tcrypt --veracrypt, losetup, mkfs.exfat -c 8K, wipefs -a, mount, umount |
helper.op_enable_units, op_add_groups, op_nm_import, op_reload |
systemctl, usermod -aG, nmcli connection reload, udevadm, sysctl --system |
recipe._services, rebuild.ServicesStep |
systemctl list-unit-files, crontab -l, id -Gn |
recipe._desktop, rebuild.DesktopStep |
dconf dump /, dconf load /, gnome-extensions list |
recipe._flatpaks |
flatpak list, flatpak remotes (the rebuild only lists Flatpaks in 0.2) |
gui/ |
GTK 4 and libadwaita 1.5 or newer, Gio.VolumeMonitor, Gio.AppInfo.launch_default_for_uri |
install.sh |
XDG folders, /usr/local/libexec/deucalion, /usr/share/polkit-1/actions |
1.4 Debian family (Debian, Ubuntu, Mint), not only Ubuntu¶
| Module and function | What |
|---|---|
helper.scan_etc, _dpkg_conffiles, _dpkg_owned_etc, _packaged_md5s |
Reads /var/lib/dpkg/status and /var/lib/dpkg/info/*.list, *.md5sums |
helper.install_time |
/var/log/installer (Ubuntu's installer), else /etc/machine-id |
recipe._apt, recipe._find_debs |
apt-mark showmanual, dpkg-query -W, apt list --installed, dpkg-deb -f |
rebuild._installed_packages, _apt_candidates |
dpkg-query -W, apt-cache policy |
helper.op_apt_* |
apt-get update, apt-get install -y --no-remove |
deps._has_package, deps.apt_install |
dpkg-query -W (not caught when missing: deucalion deps crashes on Fedora), apt-get install |
helper.op_collect_admin, op_put_crontab |
the root crontab at /var/spool/cron/crontabs/ (a Debian path) |
1.5 Ubuntu only¶
| Module and function | What |
|---|---|
aptsources.Repo.kind, is_ubuntu_mirror |
UBUNTU_HOSTS, PPA and OEM detection, the archive key name |
aptsources.map_suite |
Ubuntu codename mapping |
rebuild.DISTRO_SOURCE_FILES |
ubuntu.sources, ubuntu-esm-*.sources |
recipe.UBUNTU_KEYS, recipe.SNAP_DEPENDENCY, recipe._snaps, helper.op_snap_install |
Ubuntu keyrings, snaps |
helper.op_netplan_import, rebuild.ServicesStep (/etc/netplan/90-NM-*) |
Netplan, used by NetworkManager on Ubuntu 23.10 and newer |
deps.install_veracrypt, deps.choose_veracrypt_asset |
refuses unless ID=ubuntu; picks veracrypt-*-Ubuntu-*.deb |
cli.app_problem, gui/pages/drives.py texts |
"needs Ubuntu 24.04", "Built into Ubuntu" |
1.6 What happens today on other systems¶
| System | Result with 0.2.1 |
|---|---|
| Debian, Mint | Mostly works. Ubuntu's own sources are recognised only on Ubuntu, so Debian's own repository could be offered as "third-party" in a rebuild. Snaps are missing on Mint. |
| Fedora, openSUSE, Arch | deucalion deps crashes. Backup, restore and drives work. The recipe is nearly empty, and the /etc scan treats every file as unowned (they are hidden or unselected in the rebuild, so nothing is damaged). rebuild runs apt-get, which fails with code 127. |
| macOS | Imports work. register and setup-drive crash (findmnt). Backups could run on a registered folder, but with the Unicode risk of §4.4. |
| Windows | Nothing runs: import grp fails at start. |
2. The platform layer¶
The goal is that all platform-specific calls live in one place, with today's Linux code moved there unchanged (roadmap 0.3, steps 1–3).
deucalion/platform/
├── __init__.py current() → the Platform for this computer; Distro detection
├── base.py the interfaces (classes with the methods below)
├── linux.py today's code, moved verbatim (0.3)
├── distro.py per-family backends: deps, packages, sources, /etc scanner (0.3 – 0.4)
├── macos.py (0.6)
└── windows.py (0.8 – 0.9)
| Interface | Methods | Linux (from) | macOS | Windows |
|---|---|---|---|---|
SystemInfo |
os_info(), distro(), user(), is_admin(), hostname(), machine() |
util.os_release, pwd, os.geteuid, os.uname, /sys/class/dmi |
sw_vers, sysctl hw.model |
platform, ctypes |
Paths |
config_dir(), state_dir(), default_excludes() |
config.py (XDG) |
~/Library/Application Support/Deucalion |
%APPDATA%, %LOCALAPPDATA% |
Mounts |
mount_info(path), fs_type(path), is_posix_fs(path), mount_points(), candidate_roots(), removable_roots(), is_on_system_disk(path), is_encrypted(path) |
util.py, target.candidate_roots |
diskutil info -plist, /Volumes |
ctypes volume APIs |
Drives |
list_drives(), setup(request), open(access), close(access) |
drives.py, drivesetup.py, udisks.py |
diskutil, newfs_exfat |
PowerShell storage cmdlets (through the helper) |
Admin |
start_helper() → a connection with call(op, **args) |
privileged.Privileged (pkexec, sudo) |
sudo |
UAC + named pipe |
FileMeta |
capture(stat, path), apply(path, entry), change_time(stat), lock(path), sync(), free_space(path), cluster(path) |
os.chmod/chown, fcntl.flock, os.sync, os.statvfs |
+ extended attributes, st_flags |
attributes, msvcrt.locking, NTFS change time |
NameRules |
drive_policy(), destination_fold(path) |
names.NamePolicy |
+ Unicode normalization | + Windows name rules |
Recipe and rebuild backends |
collectors(), steps(mode) |
recipe.py, rebuild.py |
Brewfile (0.7) | later |
Rules for the move:
- Old names stay importable (
util.mount_info,util.run,drivesetup._REMOVABLE_MOUNTS, …) as thin re-exports, because tests patch them. helper.pystays self-contained and Linux-only (it runs as root withpython3 -I). macOS and Windows get their own helpers with the same JSON protocol and the same "fixed list of checked operations" design.- Characterization tests first: a fake
runthat records commands, fake system files, and the command inventory test (roadmap 0.3 step 1). The refactor is correct when these are unchanged. - The
pwd,grpandfcntlimports become lazy (inside the functions that need them).
3. Linux distributions¶
3.1 Families and detection¶
The family comes from /etc/os-release: ID, then ID_LIKE.
| Family | Distributions | ID / ID_LIKE |
|---|---|---|
debian |
Ubuntu, Debian, Linux Mint | ubuntu; debian; linuxmint with ID_LIKE="ubuntu debian" and UBUNTU_CODENAME |
fedora |
Fedora | fedora |
suse |
openSUSE Tumbleweed, Leap | opensuse-tumbleweed, opensuse-leap, ID_LIKE contains suse |
arch |
Arch | arch |
3.2 Backends per family¶
| Debian family | Fedora | openSUSE | Arch | |
|---|---|---|---|---|
| Packages for Deucalion | python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 udisks2 cryptsetup exfatprogs pkexec dconf-cli gnupg |
python3-gobject gtk4 libadwaita udisks2 cryptsetup exfatprogs polkit dconf gnupg2 |
python3-gobject typelib-1_0-Gtk-4_0 typelib-1_0-Adw-1 udisks2 cryptsetup exfatprogs polkit dconf gpg2 |
python-gobject gtk4 libadwaita udisks2 cryptsetup exfatprogs polkit dconf gnupg |
| Installed by you | apt-mark showmanual |
dnf repoquery --userinstalled |
zypper packages --userinstalled |
pacman -Qqe (AUR: pacman -Qqm) |
| Install, never removing | apt-get install -y --no-remove |
dnf install -y (never --allowerasing) |
zypper --non-interactive install |
pacman -Syu first (Arch doesn't support partial upgrades), then pacman -S --needed --noconfirm |
| Software sources | /etc/apt/sources.list(.d), keys in /etc/apt/keyrings |
/etc/yum.repos.d/*.repo, gpgkey=; COPR; RPM Fusion release packages |
/etc/zypp/repos.d/*.repo |
extra sections in /etc/pacman.conf |
| New release mapping | codenames (noble → resolute, bookworm → trixie; Mint: UBUNTU_CODENAME for third-party) |
$releasever (automatic) |
Open Build Service URLs (openSUSE_Leap_16.0, …) |
rolling, none |
/etc changes |
dpkg status conffiles + info/*.md5sums |
rpm -qa --qf '[%{FILENAMES}\t%{FILEDIGESTS}\t%{FILEFLAGS:fflags}\n]' |
same as Fedora | /var/lib/pacman/local/*/files (%FILES%, %BACKUP% with checksums) |
| Install time | /var/log/installer |
/var/log/anaconda |
/var/log/YaST2 |
first line of /var/log/pacman.log |
| Root crontab | /var/spool/cron/crontabs/root |
/var/spool/cron/root |
/var/spool/cron/tabs/root |
/var/spool/cron/root |
| Administrator group | sudo |
wheel |
wheel |
wheel |
| Desktop drive mounts | /media/$USER |
/run/media/$USER |
/run/media/$USER |
/run/media/$USER |
| VeraCrypt | official .deb (Ubuntu or Debian build) |
official .rpm + .sig |
official .rpm + .sig |
Arch's veracrypt package |
| SELinux | no (AppArmor) | yes, enforcing | yes on new installs | no |
To verify in each distribution's VM: the package names, the exact package queries (dnf 5, zypper), the openSUSE crontab and install-time paths, what each package manager does on a conflict with these options, and VeraCrypt's current asset names.
Other notes:
- openSUSE's administrator password is root's password by default (
sudoand polkit ask for the root password, not yours). The documentation and the password prompts must say so. - Linux Mint blocks
snapdwith/etc/apt/preferences.d/nosnap.pref. The rebuild then lists the snaps as "not available here". It never removes the block. - SELinux: a file written into
/etcmust get the right label. The helper runsrestoreconafterput_etc_filewhere SELinux is enabled (roadmap 0.4). - Immutable systems (Fedora Silverblue, openSUSE Aeon) have a read-only
/usr. The polkit policy in/usr/share/polkit-1/actionscan't be installed, and packages go throughrpm-ostreeortransactional-update. They are out of scope.
3.3 Rebuild modes¶
| Step | Same family | Another family (portable part) |
|---|---|---|
| Check | ✓ | ✓, and says which parts are not carried across |
| Software sources | ✓ | – never |
| Packages | ✓ (apt in 0.3; dnf, zypper, pacman in 0.4) | old list for reference; same-name suggestions unselected (0.4); Flatpaks installed |
| Home, Paths, Desktop | ✓ | ✓ |
System settings (/etc) |
✓ | – never |
| Groups | ✓ | ✓ with sudo ↔ wheel |
| Crontab, your own services | ✓ | ✓ |
| System services | ✓ | shown unselected, only for units that exist here |
| Network connections | ✓ | ✓ NetworkManager keyfiles. From Ubuntu 23.10 and newer, the generated keyfiles saved from 0.3 on (network/generated-connections/) |
Why packages don't cross families: names differ (build-essential vs a dnf group, -dev vs
-devel, split or merged packages). A guessed mapping installs the wrong things or misses some, and
nobody can review a mapping table of thousands of names. Flatpaks are the same everywhere, so they
are installed.
4. macOS¶
4.1 Drives¶
- Listing:
diskutil list -plistanddiskutil info -plist <disk>(Internal,Removable,Ejectable,BusProtocol). Mount points are under/Volumes. - The system disk: APFS shows "synthesized" disks (for example
disk3), while the physical disk isdisk0. The physical disk comes from the APFS container's physical stores (diskutil apfs list -plist). As on Linux, if it can't be found, no drive may be changed. - Formatting:
diskutil eraseDisk ExFAT <NAME> GPT <disk>.diskutilcan't choose the cluster size, so the partition is then formatted again withnewfs_exfatwith 8 KB clusters. To verify:newfs_exfat's cluster option, and whether erasing an external disk needssudo. - CI:
hdiutil create -size 2g -fs ExFAT -volname TEST test.dmgandhdiutil attachgive a real exFAT volume from Apple's driver.hdiutil attach -nomountgives a/dev/diskNthatdiskutilcan erase. Drive setup can be tested this way.
4.2 Administrator access¶
- The old Authorization Services call is deprecated. The modern way (
SMAppService, a privileged helper daemon) needs a signed app bundle. - Plan:
sudofrom the command line, with the same JSON-over-pipe helper design and its own fixed list of operations. A future app could useosascript … with administrator privileges, which has no pipe, so the helper would then need a socket. - Much less needs root on macOS than on Linux. Homebrew must not run as root.
/etc/hostsand/Library/LaunchDaemonsdo need it.
4.3 Python¶
macOS has no usable Python out of the box. /usr/bin/python3 is a stub that installs the Command Line
Tools, whose Python has long been 3.9, below Deucalion's 3.10 minimum (to verify on current macOS).
Users install Python from python.org or Homebrew. The tool copy on the drive runs with
python3 Deucalion-Tool/bin/deucalion.
4.4 How backups differ¶
- Unicode spellings. APFS treats the two spellings of
é(NFC and NFD) as the same name. Linux allows both side by side.names.fold_exfatis only.upper().- On the drive: if macOS's exFAT driver lists names in a different form than they were
written,
backup.Scanner._walkwould see real backed-up files as unknown leftovers and move them to_removed/. Nothing is lost, but the backup is damaged until the next run copies them again. The fold must also normalize, and this must be proved on a real exFAT image in CI before backups are enabled on macOS. - On restore: Linux twins (by case or by Unicode form) collide on a Mac. Today
Restorer._decidewould mark the second one "overwrite" and move the first one into the undo folder. A collision pre-check is needed (it helps Windows too).
- On the drive: if macOS's exFAT driver lists names in a different form than they were
written,
- Extended attributes and resource forks (Finder tags, quarantine flags, old resource forks): the
copy loop copies only file content. Python's
os.getxattrfamily is Linux-only, so macOS needsctypescalls to libc. Store small values in a new manifest table, and report big ones. - Files kept only in iCloud ("Optimize Mac Storage"): reading one downloads it. Skip files with the
SF_DATALESSflag (inst_flags) and warn. - Full Disk Access: without it,
~/Library/Mail,Messages,Safariand the Photos library can't be read. The scanner already skips unreadable folders and keeps what is backed up, but a first backup must explain this clearly. ~/Libraryexclusions:Caches/,Logs/,Containers/*/Data/Library/Caches/,Application Support/*/Cache*, and others, kept as a list per platform..DS_Storeand._*files written by Finder on the drive are already ignored (backup.OS_CLUTTER).
4.5 Recipe and rebuild¶
| Recipe item | How |
|---|---|
| Homebrew formulae, casks, taps, Mac App Store apps, VS Code extensions | brew bundle dump → packages/Brewfile (mas must be installed for App Store apps) |
| Apps installed by hand | /Applications (name, bundle ID, version), for reference |
| Launch agents | ~/Library/LaunchAgents (already in the home folder) |
| Scheduled tasks | crontab -l |
/etc |
/etc/hosts only (macOS has no package database to compare with) |
| Wi-Fi | not saved: passwords are in the System keychain and can't be exported without a prompt per item |
A Mac-to-Mac move is already well served by Apple's Migration Assistant and Time Machine. Deucalion's value on a Mac is a plain-file backup readable on every system, and a clean-install rebuild from the Brewfile.
4.6 Encryption¶
- LUKS: not available on macOS.
- VeraCrypt needs a FUSE layer:
- macFUSE needs a kernel extension, which on Apple silicon means lowering security in Recovery mode;
- FUSE-T needs no kernel extension; VeraCrypt has supported it since 1.26, and Homebrew has a
veracrypt-fuse-tcask that conflicts with the standardveracryptcask.
- Plan: in 0.6, Deucalion uses volumes that the user unlocked in VeraCrypt's app. In 0.7, a CI
spike decides whether
deucalion opencan drive VeraCrypt with FUSE-T (password through--stdin).
4.7 What GitHub's macOS runners can and can't prove¶
| Can | Can't |
|---|---|
The whole suite on Apple silicon (macos-15, macos-26) |
USB drives |
| Apple's exFAT driver, through disk images | Kernel extensions (macFUSE) |
| Case-sensitive and case-insensitive APFS images | Real Full Disk Access prompts on a user's machine |
diskutil on attached images |
iCloud accounts and dataless files |
Homebrew and brew bundle on a fresh machine (each job is a new runner) |
The look of a future app |
| A backup made on Linux, restored on macOS (artifacts between jobs) |
5. Windows¶
5.1 Making the code import and run¶
- Lazy
pwd,grpandfcntlimports (0.3). - Replacements in the platform layer:
os.geteuid:ctypes.windll.shell32.IsUserAnAdmin;os.statvfs:shutil.disk_usage, andGetDiskFreeSpaceWfor the cluster size;os.sync:FlushFileBufferson the files written, or nothing;os.uname: theplatformmodule;- the lock:
msvcrt.locking.
- A CI job imports every module except
helper,udisksandguion Windows (0.8, step 1).
5.2 Restoring onto Windows: the traps¶
These are real code paths in restore.py today, and each one needs a fix and a test before Windows
restores are allowed:
| Linux name or situation | What would happen | Fix |
|---|---|---|
a:b |
dest / "a:b" becomes a path relative to drive A:, outside the restore folder; on NTFS, a:b also means "stream b of file a" |
destination names through a Windows name policy; every destination checked to be inside the chosen folder |
x\y |
becomes a sub-folder | name policy |
name. or name |
Windows drops the trailing dot or space | name policy (look-alike characters, as on exFAT) |
CON, aux.txt |
reserved device names | name policy (names.exfat_safe already does this) |
Notes and notes |
the second replaces the first (moved to the undo folder) | collision pre-check |
| Paths over 260 characters | fail | \\?\ prefix |
move_aside |
undo_root / str(path).lstrip("/") gives the original path |
build the undo path from the drive letter and the rest |
| Symbolic links | need Developer Mode or administrator rights | create when allowed, otherwise list them |
| Permissions and owners | meaningless on NTFS | not applied; files inherit the folder's permissions |
5.3 How backups differ (0.9)¶
- Locked files:
NTUSER.DATis always locked, and open mailboxes and browser profiles often are. A consistent copy needs a Volume Shadow Copy, which needs administrator rights. - Change time:
st_ctimeis the creation time. UseGetFileInformationByHandleEx(FILE_BASIC_INFO.ChangeTime) throughctypes. - Permissions (ACLs): not saved. A home folder's files inherit the profile's permissions, and restoring explicit ACLs from another machine (other user IDs) would be wrong.
- Placeholders: OneDrive Files On-Demand (
FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS,FILE_ATTRIBUTE_OFFLINE) are downloaded when read. Skip them and warn. - Junctions:
os.lstattreats them as links (Python 3.8 and newer), so they are recorded as links. - The registry: not portable (user IDs, paths, program versions); never restored.
5.4 Administrator access¶
A UAC-elevated process (ShellExecuteExW with "runas") can't share stdin and stdout with the program
that started it. The helper talks over a named pipe whose access list allows only the user's SID. It
uses the same JSON protocol and the same "fixed list of checked operations" design as on Linux, and
needs its own security review.
5.5 Drives and encryption¶
- Drives:
- formatting through PowerShell (
Get-Disk,Clear-Disk,Initialize-Disk,New-Partition,Format-Volume -FileSystem exFAT -AllocationUnitSize 8192), run by the helper; - the system disk comes from
Get-Disk'sIsBootandIsSystem; - read-only work uses
ctypes(GetLogicalDrives,GetVolumeInformationW).
- formatting through PowerShell (
- VeraCrypt:
- On Windows its command line is
VeraCrypt.exe /v … /l X /p <password> /q /sand"VeraCrypt Format.exe" /create …. - As far as is known, the password goes on the command line, where other programs can read it. To verify.
- Plan: users unlock drives with VeraCrypt's own app, and Deucalion never handles the password on Windows.
- On Windows its command line is
- BitLocker To Go would be a Windows-native encrypted layout that Linux can open
(
cryptsetup --type bitlk). It is listed as "later".
5.6 Python¶
Windows has no Python by default (python opens the Microsoft Store). Users install it from
python.org or with winget install Python.Python.3.13. The official "embeddable" zip (about
10–15 MB, no installer) could be carried on the drive in Deucalion-Tool; this is decided in 0.8.
6. Testing infrastructure¶
6.1 Virtual machines for other distributions¶
docs/tools/vm.py already does the hard parts:
- QEMU/KVM, cloud-init (NoCloud over HTTP, user alex);
- SSH on a forwarded port;
- USB disk images hot-plugged over QMP (usb-bot + scsi-hd on qemu-xhci).
Two things are Ubuntu-specific:
- the image URL and its verification (SHA256SUMS.gpg with the ubuntu-cloudimage keyring);
- groups: [sudo, adm, plugdev] in the user data. Drop the line: the sudo: rule already gives
access.
Current images (checked 2026-10-12):
| Distribution | Image | Size | Verification |
|---|---|---|---|
| Ubuntu 24.04, 26.04 | already cached in ~/.cache/deucalion-docs |
— | signed SHA256SUMS (unchanged) |
| Fedora 44 | Fedora-Cloud-Base-Generic-44-1.7.x86_64.qcow2 |
not listed (about 0.5 GB) | signed Fedora-Cloud-44-1.7-x86_64-CHECKSUM, checked with Fedora's OpenPGP keys (fedora.gpg, fingerprint pinned in vm.py) |
| Debian 13 | debian-13-genericcloud-amd64.qcow2 |
326 MB | SHA512SUMS. No signature file in cloud.debian.org/images/cloud/trixie/latest/: HTTPS only, or pin the digest in vm.py when the image is first approved |
| openSUSE Tumbleweed | openSUSE-Tumbleweed-Minimal-VM.x86_64-Cloud.qcow2 |
303 MB | .sha256 + .sha256.asc (openSUSE's key, pinned) |
| Arch | Arch-Linux-x86_64-cloudimg.qcow2 |
578 MB | .SHA256 + .sig (the image builders' keys, pinned) |
| Linux Mint 22 | no cloud image; ISO about 3 GB | ~3 GB | signed sha256sum.txt; installed once by hand, kept as a base disk |
In the VMs, administrator steps run through Privileged's existing sudo -n path (no desktop, and the
user has passwordless sudo). UDisks tests need udisks2 installed in the VM.
This machine (checked 2026-10-12):
- 16 cores, 30 GB RAM (about 13 GB free at the time, with most of the swap in use);
- 95 GB free on /;
- KVM, QEMU 8.2.2, and OVMF 2024.02 with Secure Boot and Microsoft key variables (OVMF_CODE_4M.secboot.fd,
OVMF_VARS_4M.ms.fd);
- swtpm is not installed.
A Linux VM uses 2–4 GB of RAM, so about two at a time fit.
6.2 Containers in GitHub Actions¶
Containers (debian:13, fedora:44, opensuse/tumbleweed, archlinux:latest) run the unit tests with
each distribution's own Python and PyGObject. They can't mount filesystems, so the exFAT and loop-device
tests stay on the Ubuntu runner, and tests that need findmnt, lsblk or UDisks skip themselves.
6.3 Windows 11 under QEMU/KVM¶
| Item | Cost |
|---|---|
| Windows 11 Enterprise evaluation ISO (90 days, no product key) | about 6 GB (exact size read from Microsoft's page before asking) |
| Disk after installation and updates | about 30–40 GB (64 GB virtual qcow2) |
| RAM, CPUs | 6–8 GB, 4 vCPUs |
| TPM | swtpm and swtpm-tools from apt (administrator password, announced as one batch) |
| Firmware | OVMF_CODE_4M.secboot.fd + OVMF_VARS_4M.ms.fd (installed) |
| Drivers | none with emulated SATA and e1000; optionally the virtio-win ISO (~700 MB) for speed |
| Unattended setup | autounattend.xml on a small second disk: local account, OpenSSH server for scripted access |
| USB drives | the same QMP hot-plug as the Linux VMs |
It fits in the 95 GB free, but it is the biggest cost, so it is created only for 0.8 and 0.9.
The GitHub runner windows-2025 (Windows Server, free for public repositories, administrator rights)
covers the engine tests, exFAT virtual disks made with diskpart, and shadow copies.
6.4 Proposed CI matrix¶
.github/workflows/tests.yml is extended step by step: the distros job in 0.3, macos in 0.6,
windows in 0.8. Each change is shown to the owner before it is pushed.
jobs:
test: # unchanged: ubuntu-22.04, ubuntu-24.04
exfat: # unchanged: the suite on a real exFAT volume
distros: # 0.3
name: ${{ matrix.image }}
runs-on: ubuntu-24.04
container: ${{ matrix.image }}
strategy:
fail-fast: false
matrix:
image: ["debian:13", "fedora:44", "opensuse/tumbleweed", "archlinux:latest"]
steps:
- name: Install what Deucalion needs
run: |
case "${{ matrix.image }}" in
debian*) apt-get update -q && apt-get install -y -q git python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 gnupg dconf-cli ;;
fedora*) dnf install -y git python3-gobject gtk4 libadwaita gnupg2 dconf ;;
opensuse*) zypper --non-interactive install git python3-gobject typelib-1_0-Gtk-4_0 typelib-1_0-Adw-1 gpg2 dconf ;;
arch*) pacman -Syu --noconfirm git python-gobject gtk4 libadwaita gnupg dconf ;;
esac
- uses: actions/checkout@v7
- run: python3 -m unittest discover -s tests -v
macos: # 0.6
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-15, macos-26]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with: { python-version: "3.12" }
- run: python3 -m unittest discover -s tests -v
- name: The suite again on Apple's exFAT driver
run: |
hdiutil create -size 2g -fs ExFAT -volname TEST "$RUNNER_TEMP/exfat.dmg"
hdiutil attach "$RUNNER_TEMP/exfat.dmg"
DEUCALION_TEST_EXFAT=/Volumes/TEST python3 -m unittest discover -s tests -v
windows: # 0.8
runs-on: windows-2025
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with: { python-version: "3.12" }
- run: python -m unittest discover -s tests -v
Cross-platform checks pass a backup between jobs as a build artifact: made on Ubuntu, then restored on macOS (0.6) and Windows (0.8), or backed up on one macOS runner and rebuilt on another (0.7).
6.5 macOS testing options¶
Decided: GitHub's runners only (roadmap §3). The other options, for the record:
| Option | Cost (checked 2026-10-12) | Notes |
|---|---|---|
| GitHub Actions macOS runners | free for public repositories | macos-15, macos-26 (Apple silicon, 3 CPUs, 7 GB); also macos-15-intel, macos-26-intel. No USB, no kernel extensions. |
| Scaleway Mac mini M4 | €0.22/hour + tax, mandatory first 24 hours (Apple's license), or €149/month with a monthly commitment | Screen Sharing; disk images instead of USB. |
| AWS EC2 Mac | 24-hour minimum for the Dedicated Host; price not confirmed on AWS's page (a competitor quotes $1.23/hour for M4) | |
| MacinCloud | about $1/hour pay-as-you-go (from a competitor's page; not confirmed) | |
| A real Mac | — | The only way to test USB drives and real Full Disk Access prompts. |
Apple's license allows macOS virtual machines only on Apple hardware, so no macOS VM runs on the Linux test machine.
7. Packaging and distribution¶
| Platform | Now and planned | Not planned, and why |
|---|---|---|
| Linux | Run from the folder (and the copy on every drive); install.sh for any distribution (0.3); optional Open Build Service packages (.deb, .rpm, Arch) and an AUR package (0.4, with approval) |
Flatpak: the root helper, the recipe and the rebuild need the real system (host apt/dnf/dconf/systemctl, pkexec, all of /etc and $HOME). Inside Flatpak all of that goes through flatpak-spawn --host, which is a full sandbox escape, and Flathub's reviewers push back on it. Snap: the same, with classic confinement needing approval. |
| macOS | Git clone or zip, run with python3; optional Homebrew tap (0.6, with approval) |
A signed and notarized .app needs the Apple Developer Program ($99 a year) and is only useful with an app. Later. |
| Windows | Zip or Git clone, deucalion.cmd starter (0.8); maybe the embeddable Python on the drive |
Code signing: Azure Artifact Signing (about $9.99/month) is, according to DevClass, open only to organizations in the EU and UK (individuals only in the US and Canada); other options are a yearly certificate, or a free open-source signing service such as SignPath (to verify). Decided in 0.9. |
8. Facts checked on 2026-10-12¶
Prices, image names and runner labels change. Check them again before relying on them.
| Fact | Source |
|---|---|
| EC2 Mac Dedicated Hosts: 24-hour minimum allocation | AWS EC2 Mac FAQ |
| EC2 mac-m4 at $1.23/hour (third-party claim) | Green Mini comparison |
| Scaleway Mac mini prices (M4 from €0.22/hour or €149/month) | Scaleway Apple silicon pricing |
| Scaleway: mandatory first 24 hours; monthly commitment option | Scaleway: manage the commitment plan |
| MacinCloud about $1/hour (third-party claim) | Green Mini comparison |
| GitHub-hosted runner labels; free on public repositories | GitHub-hosted runners, macOS 26 runners |
| VeraCrypt with FUSE-T on macOS | Homebrew veracrypt-fuse-t, VeraCrypt macOS source log |
| Azure Artifact Signing eligibility and price | DevClass, January 2026, Azure pricing |
| Windows 11 Enterprise evaluation (90 days) | Microsoft Evaluation Center |
st_ctime on Windows deprecated; st_birthtime |
Python discussion |
| Cloud images and their checksum files | Fedora Cloud, Debian 13, openSUSE Tumbleweed, Arch |