Argos
Argos is a free-software tool for creating bootable installer USB drives, in the spirit of Rufus but built to run cross-platform.
Goal
Argos should be able to create installer USB drives for both Windows and Linux distributions, targeting both legacy MBR/BIOS and GPT/UEFI machines.
Requirements
The software should be reliable and fast, and should be installable across a wide range of Linux distributions and, ideally, on macOS as well.
Status
Argos delivers phase 3 (Windows installer media) validated on real hardware
from both hosts and on both firmwares, and phase 4 (a graphical interface)
sharing the CLI’s exact safety guarantees – see the
latest release and
CHANGELOG.md for what shipped.
- Images: Linux ISOs (including isohybrid images), written byte-for-byte
in “DD mode”; and Windows 10/11 installer ISOs, written as a FAT32 volume
with
install.wimsplit intoinstall.swmparts where it exceeds FAT32’s 4 GiB file limit. - Targets: UEFI firmware (
--layout fat32) and legacy BIOS machines (--layout fat32-bios, which carries Argos’s own MBR and FAT32 boot records). Both confirmed to reach Windows Setup’s disk selection on real machines. - Hosts: Linux and macOS, both implemented — including for Windows media,
which needs no
mkfs, no FUSE and no Windows machine anywhere in the process. Windows-as-host is out of scope for now. - Interface: a CLI (
argos) and a single-window GUI (argos-gui, currently macOS and Linux), sharing one implementation of every safety check – there is no separate, weaker path through the window. Shipped since 1.6.0; see Installation below for where each one is available, and Graphical interface for a walkthrough.
See docs/architecture.md for the full design and a
per-area status table, CHANGELOG.md for what shipped in
each release, and CONTRIBUTING.md for how to build and
test the project.
Installation
Homebrew (macOS)
brew install jp-guimaraes/argos/argos
Builds from source (there is no pre-built bottle yet); a Rust toolchain is
pulled in automatically as a build dependency. See
jp-guimaraes/homebrew-argos.
apt / .deb (Debian, Ubuntu)
Download argos_<version>_amd64.deb from the
latest release,
then:
sudo apt install ./argos_<version>_amd64.deb
A single runtime dependency (libc6) – no mkfs.ntfs, no ntfs-3g,
nothing else. See packaging/README.md for how the package is built.
Arch / pacman
Not on the AUR yet – packaging/aur/PKGBUILD is in this repository and
validated in CI, but publishing it needs the maintainer’s own AUR account
(see packaging/README.md). Until then, build it locally:
git clone https://github.com/jp-guimaraes/argos.git
cd argos/packaging/aur
makepkg -si
Pre-built binaries
For any other platform: Linux (x86_64-unknown-linux-gnu) and macOS
(aarch64-apple-darwin, x86_64-apple-darwin) tarballs are attached to each
GitHub Release – download
the tarball for your platform, extract it, and put argos and
argos-helper somewhere on your PATH (both binaries must stay in the same
directory; argos looks for argos-helper next to itself first). Neither
binary is code-signed yet, so macOS Gatekeeper will refuse to run argos on
first launch until you approve it once in System Settings -> Privacy &
Security.
GUI (macOS, .dmg)
Each release also attaches a universal Argos-<version>.dmg (Apple Silicon
and Intel in one file) – open it, drag Argos.app into Applications.
It is unsigned and unnotarized, the same decision as the CLI binaries above
and for the same reason: this project’s macOS install story is Homebrew,
which never sets the quarantine bit that triggers Gatekeeper in the first
place, so the .dmg is a convenience download rather than the primary path.
A downloaded, quarantined Argos.app will be refused on first open. Clear
the quarantine attribute once, from a terminal:
xattr -dr com.apple.quarantine /Applications/Argos.app
or, from Finder: System Settings -> Privacy & Security -> scroll to the bottom, where an “Open Anyway” button appears after the first blocked attempt.
Once running, Argos.app needs Full Disk Access to write to or
restore a removable drive – the same permission macOS requires of Disk
Utility and similar tools. Without it, a write fails right after unmounting
with opening the device: Operation not permitted. From 1.7.1 on, grant it
to the app itself: System Settings -> Privacy & Security -> Full Disk
Access -> “+” -> Applications -> Argos, and make sure its switch is on.
(Up to 1.7.0 the grant had to go to argos-helper inside the bundle, and
on the universal .dmg it could never take effect – see the 1.7.1
CHANGELOG entry.)
After every update, grant it again. Argos is ad-hoc signed, not signed
with a developer identity, so each release is a different program to macOS
and the old grant silently stops matching – the switch stays on, and
writes fail with the same Operation not permitted. The fix takes a few
seconds:
- Quit Argos (Cmd-Q).
- In Full Disk Access, select Argos and remove it with “-”. Remove any leftover argos-helper entries from older versions too.
- Add
/Applications/Argos.appagain with “+”, switch it on, and reopen Argos.
If a stale build of Argos exists elsewhere on the disk (a target/macos/
from building it yourself, an old copy in Downloads), delete it: it carries
the same app identifier, and macOS may open that copy instead of the one in
Applications. See packaging/README.md for the full story and the log
evidence.
Not yet available as a Homebrew cask – see packaging/README.md.
Via cargo install
cargo install argos-cli argos-privileged
Installs both argos (from the argos-cli crate) and argos-helper (from
the separate, privilege-separated argos-privileged crate – see
docs/architecture.md) into cargo’s install
directory (~/.cargo/bin by default), which is what puts them next to each
other. Passing only argos-cli installs argos without the helper binary it
needs at runtime – always install both together.
macOS with Homebrew’s
rustup:cargo installputs its binaries in~/.cargo/bin, and Homebrew’s keg-onlyrustupformula does not add that directory to yourPATH— the officialrustup-initinstaller does, which is why this bites Homebrew users specifically. Ifargoscomes back ascommand not foundimmediately after a successful install, that is all this is:export PATH="$HOME/.cargo/bin:$PATH"Add it to your
~/.zshrcto make it stick.
From source
git clone https://github.com/jp-guimaraes/argos.git
cd argos
cargo build --release -p argos-cli -p argos-privileged
# binaries land in target/release/argos and target/release/argos-helper
Usage
Command line
argos list
Lists every disk Argos can see and whether it looks safe to write to (not a
system disk, removable, on a USB bus). Start here – the device path you
need for the next step comes from this output (e.g. /dev/sdb on Linux,
/dev/diskN on macOS).
argos write path/to/some.iso --device /dev/sdb
# or, equivalently:
argos write --iso path/to/some.iso --device /dev/sdb
argos re-elevates itself (pkexec on Linux where available, sudo
otherwise) to run argos-helper, which does the actual write – expect a
password prompt. Before anything is touched, it prints exactly what it’s
about to do and asks you to type the device path back to confirm:
About to overwrite:
device: /dev/sdb (...)
size: ...
image: path/to/some.iso
This will PERMANENTLY ERASE all data on /dev/sdb.
Type the device path (/dev/sdb) to confirm:
A Linux or Windows installer ISO is detected automatically. Windows media
defaults to --layout fat32 (GPT/UEFI); add --layout fat32-bios for
legacy BIOS/MBR machines instead. Linux ISOs are always written
byte-for-byte (“DD mode”) and --layout is ignored for them.
argos verify /dev/sdb --iso path/to/some.iso
Re-checks a device against the image it was supposedly written from,
without writing anything – useful after the fact, or if write was run
with --no-verify. Unlike write, the device here is always positional
and the ISO is always --iso – write accepts the ISO either way
precisely so a habit formed on one command doesn’t break on the other.
argos format /dev/sdb
Turns an installer stick back into an ordinary one. A file manager’s
“Format” can’t do this on its own: media written for Windows has a
partition sized to its contents, and a Linux ISO written byte for byte
carries the image’s own partition table, so reformatting from the desktop
only touches one small volume and leaves the rest of the stick unreachable.
argos format rewrites the partition table instead – it clears what the
old image left at both ends of the device, writes an MBR with one FAT32
partition spanning all of it, and formats that, ready for Windows, macOS,
Linux, TVs and cameras. It takes seconds, and asks for the device path to
be typed back exactly like write does. --label names the volume
(default ARGOS). FAT32 cannot hold a single file over 4 GiB.
Run argos <command> --help for every flag (--no-verify, --no-eject,
--i-know-what-im-doing for a disk Argos doesn’t recognize as removable
but you’re sure about), or see the man page (argos man, or installed
automatically by the .deb/Homebrew/AUR packages above) and
argos completions <shell> for tab completion.
Graphical interface
argos-gui is a single window over the exact same safety checks as the CLI
above – there is no separate, weaker path through it. See
Installation for where to get it (macOS .dmg/Homebrew,
Linux .deb/AUR); it ships alongside argos and argos-helper in every
package.
On first launch it detects the system language automatically (LANG/
LC_ALL on Linux, AppleLanguages on macOS) – no setup needed for a
pt_BR lab machine, as pictured above. Automatic detection, English and
Português (Brasil) are also selectable by hand from the menu at the top
right, and take effect immediately, no restart:
Image: click “Choose…” or drag an ISO onto the window. Argos identifies
it as a Linux image or a Windows installer the same way argos write does,
and shows which:
For a Windows installer, an “Old machine – legacy BIOS (MBR)” checkbox
appears (disabled, with an explanatory line, until a Windows image is
chosen) – this is --layout fat32-bios from the CLI, for pre-UEFI
machines; left unchecked, media is written for UEFI (--layout fat32).
Linux images have no such choice: they are always written byte-for-byte.
Target: the dropdown lists only disks Argos considers safe to write to (removable, on a USB bus, not the system disk) – a device it refuses is a device you cannot click by accident. “Show every disk, including those the system does not consider removable” reveals the rest for inspection, but existing refusals (system disk, no removable bus) still apply; it doesn’t unlock anything, only widens what’s listed. With nothing plugged in, the dropdown says so plainly rather than showing an empty list:
Write / Verify: both stay disabled until an image and a target are
picked. “Write…” opens the same confirmation as the CLI’s “type the
device path back” prompt – retyping it is not a checkbox to click through,
on the window either. Elevation happens from there (a password dialog on
macOS, a polkit prompt on Linux); once running, progress, cancel, and any
error are reported in the window itself, translated into whichever language
is active. “Verify…” re-checks a device against an image without writing
anything, same as argos verify.
Restore drive: under the target dropdown, “Restore drive…” is
argos format from the CLI – it needs only a device, not an image, and
turns the stick back into a single FAT32 volume across its whole capacity,
with the name typed next to the button. It goes through the same
retype-the-device-path confirmation as a write.
Inspiration
At this point, it is clear that Rufus is the primary inspiration for this software. Argos aims to bring that same rock-solid reliability to a cross-platform tool that isn’t limited to Windows. After using Rufus over the years, I always thought “Rufus” sounded like a great dog name. Naturally, this project needed another great dog name to honor the tradition of pairing dependable bootable USB tools with canine names, so Argos it is! The backronym came afterward, and it is deliberately not in English: Assistente de Rebolar Gueri-gueris em Outros Sistemas
This is Brazilian Portuguese—specifically, regional slang from the Brazilian Northeast. Two words won’t survive a standard dictionary lookup: rebolar here does not mean “to shake hips/dance,” but to fling or to toss; and gueri-gueri translates to doodad or knick-knack—a small thing nobody bothers to name properly. Roughly translated, it means “Assistant for Flinging Doodads onto Other Systems”, which feels like an honest description of writing an ISO to a flash drive.
Argos was also planned as an experiment to test the current state of AI-assisted software development using agent harnesses. Rust was deliberately chosen because I had zero prior experience with it. On top of that, it involved low-level tasks requiring interaction with hardware, disk partitions, and system BIOS/UEFI. Watching the agents navigate these problems was both fun and eye-opening. A lot of tokens were burned, but a genuinely useful tool was built in less than a week.
Argos architecture
Argos creates bootable installer USB drives, in the spirit of Rufus but built to run on Linux and macOS hosts. This document describes the design and tracks the current implementation status against the product backlog. See the repository history for the full backlog (epics E0-E11) that this was planned from.
Guiding decisions (v1)
- Language: Rust.
- Interface: CLI first. The crate boundaries below are chosen so a future
GUI can sit on top of
argos-core+argos-platformwithout changes to either. - Image scope: Linux ISOs only, written byte-for-byte (“DD mode”) when the
ISO is an isohybrid image – which covers essentially every mainstream
distro today, since the image already embeds a valid MBR/GPT and BIOS/UEFI
bootloaders. Non-hybrid ISOs are refused with a clear error rather than
half-supported. Windows images (
install.wimsplitting, NTFS, UEFI:NTFS) are an explicit phase 2, not attempted here. - Hosts: Linux and macOS. Windows-as-host is a stub crate only (see below).
- Top priority: never write to the wrong disk. Every device-safety decision layers multiple independent signals rather than trusting a single OS-reported flag, and the negative test suite (never write a disk flagged as a system disk) is treated as the most important test in the project.
- No shelling out to
dd/parted/sgdisk/mkfs.gptman/mbrman/fatfs(pure Rust) cover partition-table and FAT needs for phase 2; the only accepted external-process calls are unmount/eject helpers (umount/ejecton Linux,diskutilon macOS).
Guiding decisions (phase 2: Windows ISO support, backlog #27)
v1’s “Linux ISOs only, DD mode” scope (above) was always meant to be revisited once it shipped. Windows install-media support breaks the “Argos never creates/alters a partition table” invariant v1 relied on, so – per the project’s own rule that this needed an explicit architecture decision before being touched – these were decided in a 2026-08-30 planning session, ahead of implementation, and are tracked as backlog issue #27 (sub-epics W1-W6):
- Implementation/test platform: Linux first. The highest-risk new piece is
creating a real NTFS partition, which is native and mature on Linux
(
ntfs-3g/mkfs.ntfs) but on macOS depends onntfs-3gvia Homebrew running on macFUSE (kext approval, historically fragile) – not reliably testable there. The macOS backend is deferred;argos-core’s planning logic (image classification, partition-plan arithmetic) stays host-agnostic from the start, same as v1’s split. - FAT32 4GB limit (
install.wim/.esd) workaround: UEFI:NTFS, Rufus’s current method – a small FAT32 boot partition (loads the UEFI:NTFS driver) plus a large NTFS partition holding the Windows files untouched, no splitting. The boot partition is always an exact copy ofpbatard/uefi-ntfs’s pre-built, Secure-Boot-signed.img(the same artifact Rufus vendors asres/uefi/uefi-ntfs.img), so it’s a plainddof a vendored binary –argos_core::write::dd_modealready covers that, and no FAT32-writing crate is needed. Splittinginstall.wiminto.swm(wimlib, Rufus’s older method, useful for very old BIOS-only compatibility) is deliberately deferred to the backlog. (Phase 3 update: that deferral ended – the FAT32 + WIM-split layout is now the strategic replacement for UEFI:NTFS, not a compatibility fallback; seedocs/plan-phase3-self-contained.md§2. M3 (#43) implemented the FAT32 layout as--layout fat32:partition::windows::WindowsFat32Plan(one Microsoft Basic Data partition – deliberately not ESP-typed: firmware boots removable FAT32 media by scanning for\efi\boot\bootx64.efiregardless, and basic-data is what Rufus ships, the most-travelled path), formatted and populated via pure-Rustfatfsoverargos-privileged::partition_io::PartitionWindow, a Read+Write+Seek view bounded to the partition’s byte range of the open whole-device fd – nomkfs, no mount, no partition device nodes, no partition-table reread, zero external processes in the write path. FAT32 refuses >4GiB-1 files with a dedicated error (exit 26) until M2’s WIM splitter slots into the copy pipeline – it has since landed, and M5 put FAT32 media booting real UEFI and legacy-BIOS machines to Windows Setup’s disk selection from both hosts. M4.3 decision: NTFS is retired, not merely demoted –fat32is the only layout Argos produces (plusfat32-biosfor legacy BIOS, M6); see the Status table below for the scope of what came out of the tree with it.) gptman(pure-Rust GPT),cdfs(ISO9660 reader, used here under the local dependency namecdfsbut backed by thenewtua-cdfsfork – see below), andhadris-udf(pure-Rust UDF/ECMA-167 reader, added after W1 validation showed real Windows media needs it – see the correction below) are the only new dependencies this needs. (Phase 3 update:hadris-udfhas since been replaced at runtime by Argos’s ownimage::udfmodule – see the correction below anddocs/plan-phase3-self-contained.md– and survives only as a dev-dependency fixture generator.)- This relaxes v1’s “no shelling out” rule specifically to call
mkfs.ntfs/ntfs-3gas an external process on Linux to format and mount the NTFS partition – the same posture v1 already accepted forunmount/ejecthelpers. Creating the partition table itself stays pure-Rust (gptman). cdfsdependency: thenewtua-cdfsfork, not the canonical crate. The canonicalcdfscrate on crates.io hard-depends onfuser(FUSE bindings) and aclap-based mount binary, neither of whichargos-coreuses or wants as a transitive dependency.newtua-cdfs(maintained by the same team as The Unarchiver) is a “forced fork” that strips exactly that and nothing else, publishing its library under the same crate name (cdfs) and API –argos-core’sCargo.tomldepends on it ascdfs = { package = "newtua-cdfs", ... }so the rest of the codebase is unaffected by the rename.- Correction (post-W1 validation): real Windows installer media is UDF, not
ISO9660 – this is the norm, not a rare edge case. The original planning
above treated a UDF bridge as a risk unique to unusually large multi-edition
images. Testing
image::windowsagainst a real, official Windows 10 22H2 ISO (single edition, nothing unusual) during W1 disproved that: it’s mastered as an ISO9660+UDF bridge, with the ISO9660 layer exposing only a stubREADME.TXT–bootmgr,sources/, and everything else live in the UDF layer exclusively.cdfs(ISO9660-only) could not see any of it.image::windowstherefore tries UDF first, falling back tocdfsonly for genuinely ISO9660-only Windows-shaped images – which in practice means this crate’s own synthetic test fixtures, not real media. The UDF backend was originallyhadris-udf(the reason the workspace’srust-versionmoved from 1.75 to 1.88;gptmanstayed pinned to its 1.x line regardless); phase 3’s M1 (backlog #40) replaced it with Argos’s ownimage::udfmodule afterhadris-udf’s whole-file-in-memory read caused a real OOM (#38) and its private extent resolution made streaming impossible to add from outside.hadris-udfremains a dev-dependency only, generating UDF test fixtures as an independent implementationimage::udfis verified against.
Guiding decisions (phase 4: a graphical interface, backlog #97)
Planned in docs/plan-phase4-gui.md, decided ahead of implementation the
same way phase 2’s decisions were. Tracked as backlog issue #97
(milestones G0-G9).
-
M-GUI.1 – elevation from a window: the system’s own authorization dialog over a FIFO pair, not
SMJobBless. A windowed front end has no controlling terminal, sosudo’s “a terminal is required” makes the CLI’s elevation path unusable as-is. macOS:osascript -e 'do shell script "..." with administrator privileges'puts up the system dialog, so the password never transits the Argos process and no code signing or Developer ID is needed;do shell scriptonly returns once the command finishes, so a pair of FIFOs stands in for the pipesCommand::spawnwould have given a terminal-attached process, openedO_RDWR(never blocking, and preserving the EOF-on-parent-death semanticswatch_for_canceldepends on) after an early spike found that opening them blocking hangs both ends forever if the user dismisses the dialog.argos-privilegedneeded zero lines of change for this. Linux:pkexec, which already renders the desktop’s own polkit agent, now paired with a policy (org.argos.helper.policy) naming the actual disk-write consequence instead of polkit’s generic message, andauth_adminrather thanauth_admin_keep(_keepwould let a second destructive write inside ~5 minutes proceed with no prompt at all – a safety regression for zero benefit, since writing a USB stick is not a repeated operation). Rejected:SMJobBless/SMAppService, Apple’s “correct” answer – but it makes the privileged side a persistent, launchd-managed root daemon, the opposite ofargos-helper’s one-shot design, for a paid Developer ID (US$99/yr) and notarization on every release.AuthorizationExecuteWithPrivileges(whatdo shell script ... with administrator privilegesuses underneath) has been deprecated since 10.7 and still functions – a known contingency, not a future surprise. Never elevate the GUI process itself:sudo argos-guiis not a fallback, because the privilege separation is the safety architecture. -
M-GUI.2 – a hand-written string catalogue, not gettext/Fluent.
Stringsis a plain struct: a label missing from either language is a compile error, and a parameterized message is a function pointer rather than a format template, so its placeholder arity is type-checked too – a stronger guarantee than any file-based scheme gives for two languages and roughly eighty strings. Rejected despitedocs-sitealready using PO: there is no well-maintained pure-Rust.moruntime, andgettext-rslinks Clibintl, a system dependency that directly violates the “easy to install on macOS” constraint. The escape hatch, if translator ergonomics ever matter, is abuild.rsgeneratingStringsfrom a.pofile at compile time – so the decision is not a dead end, just the simplest thing that is honest about what two languages currently need. -
M-GUI.3 – the GUI is translated; the CLI stays English-only. Its help text, man page and five shell-completion scripts all come from the same
clapdefinitions, andpackaging/build-deb.shrunsargos manon the build machine – a locale-dependentargos manwould ship whatever language the CI runner happened to have that day, not a choice anyone made.Stringsliving inargos-sessionrather thanargos-guikeeps the door open cheaply: error localization sits next toArgosErroritself, so a future front end (or a--json/localized flag on the CLI, if ever wanted) gets it for free without restructuring anything. -
M-GUI.4 – eframe 0.33, pinned, not “the latest”. Its
rust-versionis exactly this workspace’s MSRV (1.88); 0.34 needs 1.92 and 0.36 needs 1.95. It still defaults to the glow/OpenGL renderer rather than wgpu, which matters because the hosts this project serves are the same vintage as the machines it writes media for, where plain OpenGL is the safer bet than a Vulkan/Metal/DX12 stack with a GL fallback. Licence isMIT OR Apache-2.0, honouring M6.1’s refusal to relicense. The dependency-budget concession is stated rather than hidden: eframe pulls in on the order of 150-250 transitive crates, in real tension with “keep it minimal” – but the rule this project already applies toctrlctargetsargos-privilegedspecifically.argos-guiis a leaf; nothing depends on it, theargosCLI binary is unaffected, andargos-helper’s own dependency tree – the only one that ever runs as root – gains exactly zero crates from any of phase 4. Checked withcargo tree, not assumed, at each milestone that touched packaging. -
M-GUI.5 – no AppImage, no Flatpak, no code signing. AppImage/ Flatpak’s sandboxing makes raw
/dev/sdXwrites andpkexeca fight not worth having, for a tool whose entire purpose is writing to raw block devices. macOS: signing and notarizing were deliberately skipped in this phase – a paid Developer ID (US$99/yr) plus notarization on every release, for a project whose install story already has a better answer. Homebrew never sets the quarantine bit, sobrew installsidesteps Gatekeeper entirely; the unsigned universal.dmgis the convenience download for someone who wants the GUI without a Rust toolchain, withxattr -dr com.apple.quarantinedocumented for anyone who downloads it through a browser instead and hits Gatekeeper’s “Apple could not verify … is free of malware” dialog (the current, post-Ventura wording, confirmed live rather than assumed from older guides).
Crate layout
crates/
argos-core/ # pure domain logic -- no direct disk/OS I/O
argos-platform/ # the PlatformOps trait every backend implements
argos-platform-linux/ # real implementation: sysfs + udev database + /proc/mounts + UDisks2 cross-check
argos-platform-macos/ # real implementation: diskutil -plist + df
argos-platform-windows/ # deliberate stub, proves the trait has no Unix bias, out of v1 scope
argos-privileged/ # argos-helper: the one binary meant to run as root
argos-session/ # UI-agnostic orchestration + elevation + i18n, shared by both front ends
argos-cli/ # the `argos` binary
argos-gui/ # the `argos-gui` binary (phase 4, backlog #97)
argos-session (phase 4 G1, backlog #88) is the seam that makes the GUI
possible without weakening anything the CLI already guarantees: it took over
device/image preflight, Plan construction, elevation, and the helper’s
event stream from what used to be private inside argos-cli, deliberately
never printing or reading from a terminal itself – confirmation stays with
each front end, which formats the same WritePreview data its own way.
Strings/Lang (i18n, G6) live here too, next to ArgosError, rather than
in argos-gui, so error localization can match on the typed error and a
future front end gets translated messages for free.
argos-core never imports anything OS-specific for disk access; it receives
plain data (Device, byte streams, sizes) from whichever argos-platform-*
crate is selected for the current OS. That split is what makes ISO
classification, checksumming, and the safety judgement unit-testable with
ordinary files and in-memory buffers – no root, no real hardware.
argos-core
device: theDevicemodel andBusenum, plus the single safety gateDevice::is_safe_to_write()(requires: not a system disk, OS-reported removable, USB bus – all three, never just one).error:ArgosError, athiserrorenum mapped to stable CLI exit codes.progress:ProgressSinktrait and aCancelTokenfor cooperative cancellation. Cancelling never tries to “undo” a partial write – the device is reported as inconsistent and must be rewritten in full. The FAT32 Windows path goes one step further and invalidates what it leaves, because unlike DD mode its partial media would otherwise mount and look plausible.image::isohybrid: classifies an ISO from its first couple of sectors (embedded MBR signature + partition entry, El Torito boot catalog, a best-effort GPT/UEFI hint) intoHybrid/ElToritoOnly/PlainData. OnlyHybridis writable in DD mode.image::udf(phase 3, M1 / backlog #40): Argos’s own minimal, read-only UDF/ECMA-167 reader with streaming file access – anchor/volume descriptor sequence parsing (tag checksum + CRC verified on every descriptor), type-1 partition maps, File/Extended File Entries, short and long allocation descriptors, embedded data, sparse extents, and allocation-extent continuation chains. A file read materializes only the extent list and serves content in caller-sized chunks through a sharedMutex-guarded source, so a multi-GBinstall.wimcosts a few MB of RAM to copy (measured: 3.5MB peak RSS streaming a 512MB file), not its own size. Anything outside scope (metadata partitions, extended ADs, non-2048-byte blocks) is refused with a clear error, never misread.image::windows(backlog #27, W1): recognizes an official Windows installer ISO by the presence ofbootmgr+sources/boot.wimat its root, rather than fixed byte offsets, since a Windows ISO carries no embedded MBR/GPT to probe. Triesimage::udffirst, falling back tocdfs(see the phase 2 guiding decisions above, including the post-W1 correction on why UDF has to come first).WindowsIsois a thin read-only wrapper (list files with their sizes, open one by path) over whichever backend recognized the image, reused bywindows_fat32to copy the extracted files onto the FAT32 partition – both backends stream, so the copy runs in constant memory.image::checksum: streaming SHA-256, used both to fingerprint the source ISO and (once E5/E6 land) to verify what was actually written.preflight: capacity and source/target-collision checks that run in the unprivileged process before the user is even asked to confirm anything – the same pattern balenaEtcher uses in its renderer process before handing work to its privileged sidecar.check_windows_fat32_capacity(phase 3 M3, backlog #43) is the Windows-write equivalent ofcheck_capacity: it compares the device againstWindowsFat32Plan::total_bytes_requiredinstead of the raw ISO size, since the partition needs overhead margin and GPT/MBR structures on top of that. An earliercheck_windows_memoryguard (#38, found running the retired NTFS path’s W6 against real hardware) briefly guarded the UDF backend’s whole-file-in-memory read – a real Windows 10 ISO’sinstall.wimhad pushed a 7.7GB-RAM machine into memory pressure severe enough forsystemd-oomdto kill an unrelated process sharingargos-helper’s cgroup. It was retired together with the cost it guarded whenimage::udf’s streaming reader (phase 3 M1, #40) made the copy constant-memory; its exit code (25) stays reserved rather than reused.partition::windows(phase 3 M3/M6, backlog #43/#45): pure arithmetic, no disk I/O.WindowsFat32Plan::new(GPT) andWindowsMbrPlan::new(MBR, M6) lay out the single FAT32 partition – a 1 MiB-aligned start (the same convention Windows Setup/Rufus/parteduse), a sector-rounded size with a fixed overhead margin on top of the extracted (and, where needed, WIM-split) files’ raw byte total (deliberately generous and uncalibrated; M5’s real-hardware pass is what told us it didn’t need adjusting) – andtotal_bytes_requiredfolds in the GPT/MBR structure overhead for the capacity preflight check above.windows_fat32turns either plan into a real partition table viagptman/mbrman.verify(phase 3 M3, backlog #43):verify_windows_fat32_layoutandverify_windows_file_hashare the Windows-write path’s counterpart toverify_written_imageabove – deliberately not a reuse of it, since that function assumes one meaningful whole-device hash, and a partitioned write has none. Both are pure comparisons over plain data (ObservedPartitionwraps a partition type GUID + region, carrying nogptmantype – onlyargos-privileged, which does the actual reading, links that crate), so they’re unit-tested the same way as everything else here: no disk, no privilege.argos_privileged::windows_fat32::execute_verify_windows_fat32is what actually reads a real GPT/MBR and the FAT32 filesystem and calls into these.
argos-platform / argos-platform-linux
PlatformOps is intentionally small and free of Unix-specific assumptions (no
/dev/sdX parsing baked into the trait) so a real Windows backend could
implement it later without the trait changing.
Three methods the NTFS write path added (backlog #27, W3) –
reread_partition_table, mount_ntfs_partition, unmount_path – were
removed from the trait and every backend along with that path itself
(decision point M4.3): the FAT32 layout needs none of them, writing directly
into the partition’s byte range of the open whole-device fd instead of
reading/formatting/mounting a filesystem node.
The Linux backend enumerates disks by reading /sys/block/* directly (size,
removable flag, vendor/model) and cross-referencing the udev database at
/run/udev/data/b<major>:<minor> for a more reliable bus classification and
serial number, when udev has recorded the device. Reading the udev database as
flat text files – rather than linking libudev via bindgen – is a
deliberate v1 simplification: no extra system libraries needed to build.
That sysfs/udev verdict is then cross-checked against UDisks2 over D-Bus
(udisks2.rs, using zbus’s blocking API), when udisksd is reachable: this
is the “two sources, cross-referenced” defense in depth from the original
design notes, matching what desktop file managers show. The cross-check can
only push the result to be more conservative, never less – if UDisks2
disagrees that a device is a removable USB disk, os_reports_removable is
cleared regardless of what sysfs/udev concluded on their own. When UDisks2
isn’t running (headless servers, containers, minimal installs) or the D-Bus
call fails for any reason, Udisks2Snapshot::fetch() returns None and
enumeration falls back to sysfs/udev alone, unchanged from before this
existed. One non-obvious wrinkle found while wiring this up against this
machine’s real udisksd: a single UDisks2 Drive object backs several
Block objects (the whole disk plus each of its partitions, all pointing at
the same Drive), so building the device-path lookup has to key on the block
device that lacks a Partition interface, not on the drive object path
itself (an earlier attempt collapsed a disk’s own entry to whichever
partition happened to be processed last).
System-disk detection parses /proc/mounts and flags a disk as a system disk
if any of its partitions is mounted at /, /boot, /boot/efi, or /home.
This is a second, independent signal on top of the bus/removable check in
Device::is_safe_to_write – a disk must clear both to be offered for
writing. Mount sources are resolved through any device-mapper stack in
between (dm.rs) before that check: LVM, software RAID, dm-crypt, and
multipath are all just dm-N block devices from the kernel’s perspective,
and /proc/mounts only ever shows the top of that stack (e.g.
/dev/mapper/vg-home), never the physical partition underneath. Without this
resolution, a disk holding an LVM physical volume for /home – a common
desktop Linux layout – would never be recognized as a system disk, and an
ISO stored on such a filesystem would never trip the source/target collision
check either. The recursive walk itself is pure and unit-tested with fake
multi-level stacks (LVM, dm-crypt-under-LVM, striped volumes); reading the
real /sys/block/*/slaves relationship and resolving a /dev/mapper/*
symlink to its dm-N target are validated against a real dmsetup +
loop-device stack in tests/dm_resolution.rs (root-gated, mirroring
argos-privileged’s loop-device tests).
argos-platform-macos
Enumerates disks via diskutil list -plist (top-level WholeDisks) plus one
diskutil info -plist <id> per disk, parsed defensively in diskutil.rs (a
missing/renamed key degrades to a documented default rather than panicking or
failing the listing – covers the “diskutil’s plist schema changes between
macOS versions” risk called out in the backlog). Synthesized APFS-container
pseudo-disks (VirtualOrPhysical == "Virtual") are excluded outright, the
same way argos-platform-linux drops loop/dm/md/zram entries – they aren’t a
real, independently writable block device. An internal physical disk is
still returned, though: system-disk detection cross-references diskutil info -plist /, walking through its APFS container to the physical store
backing it (the Apple Silicon case the backlog flags – the boot volume’s
ParentWholeDisk is a virtual container, not the real internal SSD, until
that walk happens) to flag the true system disk, while RemovableMedia and
BusProtocol (only "USB" maps to Bus::Usb) independently keep any
internal disk from passing Device::is_safe_to_write even if that
cross-reference ever fails. unmount/eject shell out to diskutil unmountDisk/diskutil eject; backing_device_of shells out to df -P,
since (unlike diskutil info) it accepts an arbitrary file path rather than
just a device identifier or a volume’s own mount point.
Verified end-to-end on a real Apple Silicon Mac, both internal and external:
unit tests on the parsing/decision logic (fixtures captured verbatim from
this machine’s diskutil output, including a real external USB stick –
the previously-synthetic external fixture has since been confirmed against
real hardware), plus manual runs of the full PlatformOps surface against
this machine’s real disks. list_removable_disks correctly enumerated the
internal disk (filtered of its three APFS-container pseudo-disks, flagged as
the system disk via the container walk-up, is_safe_to_write() == false)
alongside a plugged-in USB stick (bus == Usb, is_safe_to_write() == true); refresh re-resolved the USB stick by platform id; backing_device_of
resolved a real file’s backing device; unmount cleanly unmounted both of
the USB stick’s partitions (diskutil unmountDisk); eject logically
removed it from the OS (diskutil eject), confirmed by it disappearing from
diskutil list.
The DD-mode write itself (argos-helper, separate from E3’s enumeration
scope) has since been verified too: argos write against a real physical
USB drive, using the real Alpine Linux 3.24.1 (virt flavor) ISO. The write
completed, argos-helper’s own post-write verification passed, and the
written bytes were independently re-read straight off /dev/diskN with
sudo dd | shasum -a 256 (outside Argos entirely) and matched Alpine’s
published SHA-256 exactly. macOS additionally popped its own “disk not
readable” dialog afterward (Disk Arbitration not recognizing the freshly
Linux-formatted disk) – expected and harmless, same as any raw dd-written
Linux USB on macOS; dismissed with Ignore, never Initialize.
That Alpine virt drive booted on the test UEFI machine (a Microsoft
Surface) but hung part way through the kernel’s own hardware
initialization, past the point Argos or the bootloader are involved –
consistent with virt’s minimal driver set meeting Surface’s
non-standard firmware/controllers, not with a bad write (the byte-exact
re-hash above already rules that out). A second write to the same drive,
this time a real, official Ubuntu 22.04.5 LTS Desktop ISO
(checksum-verified against Canonical’s published SHA256SUMS before
writing; argos-helper’s own post-write verification passing was this
run’s write-correctness check, rather than a second external re-hash),
booted successfully on that same Surface – full live GNOME session, not
just a bootloader handoff. Confirms the DD-mode write path end-to-end on
macOS with real, popular-distro media, matching what Linux already
confirmed with the same ISO family.
argos-platform-windows
Returns ArgosError::NotImplemented from every method and has no public
constructor – it exists only so argos-cli already depends on the trait and
picks a backend via #[cfg(target_os = ...)], and so the trait itself is
proven not to have crept in a Unix bias. It cannot be instantiated, only
type-checked, since Windows-as-host is out of v1 scope entirely.
argos-privileged
This is argos-helper, the one binary that runs as root. It reads a single
protocol::WritePlan as JSON on
stdin, re-resolves the target device by serial + size through the platform
backend (protocol::validate_refreshed_device is the TOCTOU guard: it refuses
the write if the device changed, disappeared, or now looks like a system disk,
regardless of what the plan claims), then runs the DD-mode write and (unless
--no-verify was passed) the post-write verification, reporting progress and
the outcome as one JSON Event per stdout line. It parses no ISO and talks to
no D-Bus/plist/UDisks2 API. The crate is split into a library (protocol,
reused by argos-cli to build plans and parse events) and the argos-helper
binary that is the only thing here meant to actually run privileged.
Cancellation is wired end-to-end (M7.5, backlog #35). argos keeps the
helper’s stdin open for the whole write instead of closing it after the plan
line; a SIGINT handler writes protocol::CANCEL_SIGNAL into it, and a
watcher thread in argos-helper turns that byte – or the pipe simply closing,
which is what a parent that died outright leaves behind – into a
CancelToken the write loop checks on every buffer.
The helper ignores SIGINT itself. Ctrl-C in a terminal goes to the whole
foreground process group and the privilege broker leaves the helper in it, so
the default disposition would kill it mid-write, before the write path could
act on the cancellation at all. Cancellation therefore has exactly one
channel, and the helper stays alive long enough to use it – which matters
because the FAT32 path does real work on the way out: it destroys the volume’s
boot sectors, so half-written media cannot be mistaken for good media.
SIGKILL still cannot be caught, and media left behind that way is exactly
what ArgosError::Cancelled has always described.
The NTFS write path (backlog #27, W3/W4) was retired at decision point
M4.3, once the FAT32 layout below was validated on real hardware from both
hosts, on both firmwares (see the phase 2 guiding decisions above, and
docs/plan-phase3-self-contained.md §5 / docs/plan-linux-validation.md for
the record of that validation). windows::execute_write_windows_image/
execute_verify_windows_image, the WindowsPartitionPlan two-partition
layout, the vendored uefi-ntfs.img boot image, the mkfs.ntfs/ntfs-3g
shell-outs, and the three PlatformOps methods that existed only to support
them (reread_partition_table, mount_ntfs_partition, unmount_path) are
all gone from the tree, not merely superseded in the CLI’s default. Everyone
sending Argos a Plan::WriteWindowsImage/VerifyWindowsImage with
WindowsLayout::Fat32/Fat32Bios – the only variants left – reaches
windows_fat32::execute_write_windows_fat32/execute_verify_windows_fat32
instead; an old plan JSON with no layout key at all now defaults to
Fat32 rather than the retired Ntfs (protocol.rs’s backward-compatible
#[serde(default)] still parses it, it simply means something different now
than it did before M4.3, since the old meaning no longer exists to preserve).
windows_fat32::execute_write_windows_fat32 is execute’s Windows-write
equivalent, dispatched via the same WriteWindowsImage Plan variant. In
one privileged elevation – CONTRIBUTING.md’s scoped exception to this
crate’s “keep it minimal” rule covers exactly this – it re-validates the
device (validate_refreshed_device_for_windows_write), re-classifies and
re-lists the source ISO itself (never trusting the plan’s idea of what’s on
it), plans which files copy verbatim and which need WIM-splitting
(plan_copy_actions, phase 3 M2.3/M3.5, backlog #42/#43), builds a
WindowsFat32Plan/WindowsMbrPlan (GPT or MBR, per plan.layout), writes
the partition table via gptman/mbrman, then formats and populates the
one FAT32 partition through fatfs over a
partition_io::PartitionWindow/SizedDevice – a Read+Write+Seek view
bounded to the partition’s byte range of the open whole-device fd. No
mkfs, no mount, no partition device nodes, no partition-table reread: the
whole write is this process talking to one file descriptor. Every file is
hashed in the same pass it’s copied (image::checksum::copy_and_hash); an
oversized install.wim streams through image::wim’s splitter into .swm
parts instead (UDF read -> splitter -> fatfs write, still one pass).
repair_directory_entries runs after the copy – see below for why.
Exercised against a real file-backed loop device in
crates/argos-privileged/tests/write_windows_fat32.rs (root + losetup
gated, no mkfs.ntfs/ntfs-3g/--partscan needed – that difference from
the retired NTFS path is M3’s acceptance criterion), and validated on
real hardware from both hosts (see above).
windows_fat32::execute_verify_windows_fat32 is the same write path’s
verification counterpart, dispatched via VerifyWindowsImage, following the
same read-only posture execute_verify’s VerifyPlan established (no
TOCTOU refusal window, no expected_serial/expected_size_bytes). It
re-derives the expected layout from the source ISO exactly like the write
path does, reads the real GPT/MBR off the device and checks it against the
plan (argos_core::verify::verify_windows_fat32_layout/
verify_mbr_layout), then reads the FAT32 filesystem back – read-only,
still no mount – and hashes every file WindowsIso lists against a fresh
read of the source ISO (argos_core::verify::verify_windows_file_hash, one
call per file).
format::execute_format is the Plan::Format handler behind argos format and the GUI’s “Restore drive…”: it returns a stick to ordinary use
rather than writing an image. The same TOCTOU guard as a write
(validate_refreshed_device_for_format), then, over the same
BufferedDevice/SizedDevice stack on one exclusively-opened fd: zero the
first and last MiB (Phase::Wiping – old boot code, GPT headers at either
end, an ISO9660 descriptor), write an MBR with one inactive FAT32 (LBA)
partition from 1 MiB to the device’s last whole sector
(windows_fat32::write_single_mbr_partition, which the BIOS Windows path now
also goes through), format it (windows_fat32::format_fat32_volume, likewise
shared, so volume IDs and the 255x63 geometry can’t drift between the two),
record the partition start in the BPB, and read the result back. The layout
(argos_core::partition::format::whole_device_fat32_region) and the label
are re-derived in the helper from the device’s refreshed size and the plan’s
raw label, never trusted from the caller. MBR only, by design: the point is
a stick every OS, camera, TV and old BIOS reads, and a partition spanning a
USB stick never needs what GPT adds. Bounded to 512 MiB–2 TiB, the forced
FAT32 floor and the MBR’s 32-bit sector fields; exFAT (for files over 4 GiB)
needs its own formatter, since fatfs has none, and is a follow-up.
Exercised in-process by format’s unit tests and, against a real macOS block
device dirtied with a DD’d hybrid ISO, by
crates/argos-privileged/tests/hdiutil_format.rs, which re-attaches the
image afterwards and runs fsck_msdos on it.
fatfs’s directory-entry defects, and why the repair pass stays
(phase 3 L4, backlog #56). fatfs 0.3.6 writes two things the FAT
specification forbids: long-filename entries in front of . and .., which
must be a directory’s first two entries, and a .. pointing at the root’s
real cluster where the specification requires zero when the parent is the
root. Both are repaired after the copy by
windows_fat32::repair_directory_entries rather than avoided at the source.
The decision was between tracking an upstream fix, vendoring a patch, and
keeping that repair pass. The repair pass stays, for now, because the
upstream fix exists but is unreleased: crates.io serves 0.3.6, while
rust-fatfs master – an unreleased
0.4.0, actively maintained – already fixes both defects, each exactly where
the analysis in #56 pointed (create_dir asks is_root_dir() before filling
in ..; write_entry skips the long-name entries for . and .., its own
comment noting they “need to be at the first two slots”). Verified rather than
read: the same reproduction program compiled against each version writes ..
as cluster 2 under 0.3.6 and cluster 0 under master, and fsck.vfat -n exits
1 on the first and 0 with clean output on the second. Master also initializes
the FSINFO free-cluster count, which is why our media still draws that one
remaining (spec-legal, status-neutral) fsck note.
So there is nothing to contribute upstream and nothing to vendor – only a
release to wait for. Adopting master early is not a version bump: it replaced
the I/O surface with its own IoBase/ReadWriteSeek traits (std types go
through StdIoWrapper), so windows_fat32 would need reworking. When 0.4.0
ships, repair_directory_entries and the FSINFO note should go with it, and
the FAT conformance tests on both hosts are what will say whether they can.
argos-session
Everything that happens between a user asking for a write and argos-helper
doing one, with no opinion about how the asking looked. prepare_write
resolves the image to an absolute path, refreshes the device, applies the
non-negotiable safety gate (check_device_is_offerable), classifies the image
DD-mode-first, runs the preflight checks, and returns a PreparedWrite
carrying the Plan plus a WritePreview – the facts a confirmation prompt
needs, as data rather than as a formatted block. spawn then elevates and
hands over the Plan, and Running::stream drains the helper’s events into
an EventSink.
Two shape decisions worth naming. spawn and stream are separate calls, so
the caller holds a Canceller before it blocks on the event stream; that is
what lets a GUI button cancel a write, where argos-cli previously needed a
SIGINT handler reaching into a mutex. And confirmation is deliberately not
here: each front end owns its own, which is why the retyped-device-path prompt
still lives in argos-cli.
The safety argument for the crate is that there is now one implementation of
“may I write to this disk?” rather than one per interface. A front end cannot
weaken it by forgetting a step, and argos-helper re-validates everything
regardless (protocol::validate_refreshed_device).
ElevationUi picks how the user is asked to authorize. Terminal is what
argos has always done – pkexec where it exists on Linux, sudo
otherwise, over the child’s own pipes. Graphical exists because a windowed
front end has no controlling terminal, and sudo then has nowhere to read a
password from at all: it fails outright with “a terminal is required”. On
Linux that just means requiring pkexec, which renders the desktop’s own
polkit agent; packaging/linux/org.argos.helper.policy gives that dialog a
message about erasing a disk instead of pkexec’s generic one, in English and
Brazilian Portuguese.
macOS has no equivalent, so the graphical route runs osascript’s do shell script ... with administrator privileges, which puts up the system’s own
authorization dialog – Argos never sees the password, and none of it
needs code signing or a Developer ID. The cost is that do shell script
returns stdout only once the command has finished, which would take both the
progress bar and cancellation with it. A pair of FIFOs stands in for the
pipes: sh runs as root with < plan > events, so the helper’s stdin and
stdout are those FIFOs and argos-privileged needs no change at all –
the cancel byte, the EOF-means-cancel safety net and the JSONL stream all
behave exactly as they do over a pipe. Confirmed by writing a real ISO
through it: byte-identical output to the terminal route, and a media hash
matching the source.
Both FIFOs are opened O_RDWR, which never blocks, and that is load-bearing
rather than tidy. Opening each end blocking looks like a clean handshake with
sh’s two redirections, and is one – until the user dismisses the
authorization dialog. Then osascript exits, sh never runs, no counterpart
end is ever opened, and both opens block forever; observed for real, with the
parent still alive and stuck after the child was gone. O_RDWR also preserves
the EOF-on-parent-death semantics watch_for_cancel relies on, since what
makes the helper’s read() return 0 is the last write end closing.
A signed, launchd-managed privileged helper (SMJobBless/SMAppService) is
Apple’s own answer and was declined: it costs a paid Developer ID and
notarization on every release, and it would make the privileged side a
persistent root daemon – the opposite of argos-helper’s one-shot design.
sudo -A with an osascript askpass is kept as the documented fallback, and
rejected as the default because the password would then transit the Argos
process.
argos-cli
argos list lists every physical disk visible to the current platform backend
and marks whether each is safe to write to.
argos write runs the full flow: refresh the target device, apply the
non-negotiable safety gate (a system disk is refused unconditionally; a
non-removable disk needs --i-know-what-im-doing), classify the ISO (refusing
anything that isn’t a hybrid image), run the capacity and source/target
collision preflight checks, require the user to retype the exact device path
to confirm, then hand a WritePlan to argos-helper via pkexec (preferred
on Linux) or sudo, rendering its progress as an indicatif bar, then ejects
the device (--no-eject skips this) – best-effort, the same posture the
PlatformOps::eject implementations already take internally, so a failed
eject warns rather than turning an otherwise-successful write into a
failure. Notably, argos write still does not call unmount before
opening the device – see the Status table below.
argos verify re-runs post-write verification against a device without
writing again, hashing the ISO (the Checksumming phase) and comparing it
against a fresh read of the device (argos_core::verify), via the same
argos-helper elevation path argos write uses – reading a raw device
needs the same privilege writing does. The argos<->argos-helper IPC is a
tagged Plan (Write/Verify/WriteWindowsImage/VerifyWindowsImage)
rather than always a WritePlan.
Both commands classify the image DD-mode-first (backlog #27, W5): if
image::classify doesn’t recognize a hybrid Linux image, they try
image::windows::classify next, and only fall through to UnsupportedIso
if neither matches. On a Windows-installer match, write computes the same
WindowsPartitionPlan execute_write_windows_image will independently
recompute (purely for display – the privileged side never trusts it) and
shows the two-partition layout it’s about to create in place of a single
image size, before the same retype-the-device-path confirmation; --no-verify
has no effect on a Windows write, since execute_write_windows_image never
verifies inline (see its own doc comment for why splitting that into a
second privileged call wasn’t worth it) – write prints a reminder to run
argos verify afterward instead. Neither command attempts a Windows
image on a non-Linux host at all: both check cfg!(target_os = "linux")
themselves, before any preflight work or confirmation prompt, and fail with
a specific WindowsImageRequiresLinux error rather than only discovering
NotImplemented deep inside argos-helper after an elevation prompt.
Status
| Area | Status |
|---|---|
| Domain model, errors, progress/cancellation, ISO classification, checksum, preflight checks | Implemented, unit-tested |
| DD-mode write engine, post-write verification | Implemented, unit-tested |
| Linux disk enumeration | Implemented (sysfs + udev database, cross-checked against UDisks2/D-Bus when reachable) and LVM/RAID/dm-crypt-aware system-disk detection; pure parsing/resolution logic unit-tested, and the D-Bus and device-mapper glue each confirmed against this machine’s real udisksd and a real dmsetup stack |
macOS disk enumeration (diskutil -plist) | Implemented, unit-tested; manually verified end-to-end (list/refresh/unmount/eject/backing_device_of) against a real Mac, both its internal disk and a plugged-in USB stick |
Privileged helper (argos-helper) | Implemented; end-to-end write+verify passes against a real file-backed Linux loop device, a real macOS hdiutil-attached disk image, and real physical USB drives on both Linux and macOS, including the TOCTOU re-validation guard in each case |
argos list / argos write | Implemented and manually verified against real physical USB hardware on both platforms. Linux: first with a synthetic isohybrid-signed image, then with a real, official Ubuntu 26.04.1 Desktop ISO (checksum-verified against Canonical’s SHA256SUMS) written byte-for-byte: device detection, confirmation flow, pkexec elevation, write, and post-write verification all passed, and the written bytes were independently re-hashed outside Argos and matched the official ISO checksum exactly; the resulting drive was confirmed to boot for real on UEFI. macOS: a real, official Alpine Linux 3.24.1 (virt) ISO (checksum-verified against Alpine’s published sha256) written the same way, with the same independent sudo dd | shasum re-hash matching exactly (that drive booted but hung mid-kernel-init on the UEFI test machine, a Surface – consistent with virt’s minimal driver set, not a bad write); a second write of a real, official Ubuntu 22.04.5 LTS Desktop ISO (checksum-verified, argos-helper’s own post-write verification passing) to the same drive booted successfully on that same Surface, full live session. argos write now ejects the device automatically after a successful write (--no-eject to skip), and argos-helper now unmounts it immediately before opening it for write (the Unmounting phase) – closing #20, the safe-open precondition the guiding decisions above call for, which nothing called until now. A no-op, not an error, when nothing was mounted. A third macOS write, a real official Ubuntu 18.04.5 LTS Desktop ISO (checksum-verified against Canonical’s published SHA256SUMS) written to the same physical USB drive, was carried to a real, old BIOS/legacy machine (no UEFI at all) and booted successfully in legacy MBR mode – confirming the last untested boot path for v1.0 (BIOS/legacy on Linux is still separately unconfirmed, but macOS-written media now covers both UEFI and BIOS). Progress feedback (indicatif) is currently invisible when stdout isn’t a real terminal – tracked separately. |
argos verify (standalone) | Implemented. execute_verify’s core logic is confirmed for real against both a matching write and a mismatched device/ISO pair (ChecksumMismatch), via the E9 hdiutil-image tests on macOS (Linux loop-device equivalents written the same way, exercised by CI). The full CLI path – device resolution, sudo elevation, progress bar, final printout – was manually run end-to-end on this Mac against a real physical USB drive: argos write then a separate argos verify invocation both reported the same SHA-256 (e73a6241...), matching Alpine’s published checksum. |
| Windows ISO support (backlog #27) | W1-W5 implemented: W1 (image::windows: UDF-first/ISO9660-fallback detection + read-only file-tree wrapper – corrected mid-implementation after real-media testing showed official Windows ISOs are UDF bridges, not plain ISO9660), W2 (partition::windows::WindowsPartitionPlan: two-partition layout arithmetic + preflight::check_windows_capacity), W3 (argos-privileged::windows: real GPT via gptman, vendored UEFI:NTFS boot image, mkfs.ntfs/ntfs-3g shell-outs, per-file copy+hash), W4 (execute_verify_windows_image: GPT layout + boot partition + per-file hash verification), and W5 (argos write/argos verify both classify DD-mode-first then try the Windows-installer shape, showing the two-partition layout before confirming, refusing early and honestly on non-Linux hosts). W1 confirmed end-to-end (classify, list 906 files, extract and byte-verify individual files including a 5.18GB install.wim listed correctly) against a real, official Microsoft Windows 10 22H2 ISO; W5’s classification/layout/preflight logic re-confirmed against that same real ISO (correctly routed as non-DD/Windows-installer, correct two-partition layout and capacity pass/fail at plausible USB stick sizes). W2-W4 unit-tested; W3/W4’s real-loop-device integration tests (root/losetup/mkfs.ntfs/ntfs-3g-gated) confirmed passing for real in CI. First real-hardware W6 attempt (real Windows 10 ISO to a physical USB drive) surfaced a real memory-exhaustion bug (#38, install.wim’s whole-file-in-memory UDF read plus a memory-constrained machine OOM-killed an unrelated process); first mitigated with a check_windows_memory preflight refusal, then fixed for real by image::udf, Argos’s own streaming UDF reader (phase 3 M1, #40 – constant-memory copy confirmed at 3.5MB peak RSS streaming a 512MB fixture file; the preflight guard and hadris-udf runtime dependency were retired with it). image::udf since re-validated against both real official ISOs (M1.5): Windows 10 22H2 (5.18GB install.wim) and Windows 11 25H2 (7.58GB install.wim, checksum-verified against Microsoft’s published SHA-256), each streamed at 3MiB peak RSS with a digest byte-identical to macOS’s own native UDF driver reading the same file. Phase 3 M3 (#43) added the pure-Rust FAT32 single-partition layout behind `–layout fat32 |
| Packaging/distribution | GitHub Releases binaries (x86_64-unknown-linux-gnu, aarch64-apple-darwin, x86_64-apple-darwin) implemented via .github/workflows/release.yml, triggered by a vX.Y.Z tag push, now building and shipping argos-gui alongside argos/argos-helper in every tarball, the .deb, and a lipo’d universal .dmg. The Homebrew tap (jp-guimaraes/homebrew-argos) exists and is bumped automatically by the release workflow (update-homebrew-tap) – reported from real use: the tap sat pinned to an old version for several releases because bumping it was a manual step nobody remembered. The AUR PKGBUILD still builds argos-cli/argos-privileged only: its source= deliberately points at a tagged release tarball, to prove the real sha256sums and build steps work against what the AUR would actually fetch rather than this checkout, and no tag published so far contains argos-gui – build()’s own comment in the PKGBUILD spells out exactly what to add (-p argos-gui, the three desktop-integration assets, two new runtime depends) once one does. crates.io publish not started – needs a decision/credentials only the project owner has. |
Phase 4: graphical interface (argos-gui, backlog #97) | G0-G8 implemented; G9 (real-hardware validation) in progress. One window sharing every safety check with the CLI through argos-session (extracted in G1): device refusal, TOCTOU re-validation, retype-the-device-path confirmation. Elevation (G3) validated end-to-end on both hosts against real removable media – macOS needed argos-helper granted Full Disk Access (TCC; the same requirement Disk Utility, CleanMyMac and balenaEtcher carry), confirmed by log stream showing the exact authorization flip; Linux’s pkexec route confirmed with a human actually dismissing the polkit dialog (exit 126, ElevationDeclined) and with a real write completing end to end. i18n (G6) covers the whole GUI surface in English and Brazilian Portuguese, including translated error categories for the ~20 stable argos-helper exit codes; every glyph the UI can draw in either language is checked against the actually-bundled fonts by every_glyph_the_ui_draws_is_renderable_with_the_bundled_fonts, not assumed. Real-hardware validation on Linux (2026-09-09, Ubuntu 24.04, GNOME 46, X11, package-installed build): a full write+verify+eject cycle from a real Ubuntu 24.04.2 ISO to a physical USB stick, launched from the applications menu with no terminal attached; a genuine mid-flush USB unplug producing a translated I/O-error panel with the window remaining fully responsive (not the terminal-only crash path); a cancel press landing inside the ~2%-of-wall-clock cancellable window (#104) and settling as Cancelled; a SIGKILL of argos-gui alone while argos-helper was mid-fsync, with the helper running to completion on its own via the EOF safety net rather than hanging or corrupting the target – the same mechanism the G0 spike proved on macOS, now confirmed on the Linux pkexec route too; and automatic language detection from a real pt_BR.UTF-8 locale, with no ARGOS_LANG override and no saved config, rendering the entire window in Portuguese including every accented character. One real defect found this way and fixed (not merely noted): a Helper-sourced error with a known exit-code category was showing its translated category glued to the entire raw English message on the same, un-collapsible line (operação cancelada: operation cancelled by user; the device is left in an inconsistent state...) – every other ArgosError variant already kept the raw text confined to the “Details” pane; the Helper branch was the one exception, now fixed to match. Still open for G9: the same disconnect/cancel/force-quit matrix on macOS, Gatekeeper’s exact on-screen wording for this specific unsigned build, and whether Full Disk Access survives a rebuild of argos-helper (#107) – relevant because every release recompiles it. |
Prior art consulted
- Popsicle (Rust, PopOS) – closest prior art in the same language; confirmed D-Bus/UDisks2 as a viable Linux enumeration path and the CLI/core workspace split.
- balenaEtcher – confirmed the privileged-sidecar
pattern (
argos-helper) and the pre-write capacity/source-target-collision checks now inargos_core::preflight. - Ventoy – reference disk layout (protective MBR + GPT + separate ESP) for a possible future multi-ISO / persistent partition mode, out of v1 scope.
Contributing to Argos
Argos is early-stage; the current state of the project is tracked as a backlog
of epics and stories in docs/architecture.md, which
also documents the design decisions behind the current crate layout.
Getting set up
- Install a stable Rust toolchain (1.88+). Via
mise:mise use -g rust@latest. Viarustupworks too. cargo build --workspacecargo test --workspace- Before sending a change:
cargo fmt --allandcargo clippy --workspace --all-targets -- -D warnings(this is exactly what CI runs). - The write-path integration tests exercise a real file-backed loop device and
need root:
sudo -E cargo test -p argos-privileged --features test-overrides --test loop_device_write -- --ignored --nocapture. They are#[ignore]d and skip themselves cleanly without root, so the regularcargo test --workspaceabove never needs it.
Workspace layout
crates/argos-core– all platform-agnostic logic (device model, ISO classification, checksums, the write engine). No direct disk/OS I/O; must stay testable with plain files and in-memory buffers.crates/argos-platform– thePlatformOpstrait every OS backend implements.crates/argos-platform-linux,crates/argos-platform-macos,crates/argos-platform-windows– OS-specific implementations of that trait. The Windows one is a deliberate stub (out of v1 scope) that exists only to keep the trait honestly cross-platform.crates/argos-privileged–argos-helper, the one binary meant to run as root. Keep it minimal and dumb by design (see the doc comment in itsmain.rs) – do not add D-Bus or other heavy dependencies to this crate. Scoped exception (backlog #27, decided when W3 landed; the write path itself was replaced by phase 3’s pure-Rust FAT32 layout, decision point M4.3, but the exception still holds for it): the Windows installer write path does read the source ISO’s file tree here, viaargos_core::image::windows/cdfs, so it can copy those files onto the freshly-formatted FAT32 partition and hash each one as it goes – in a single privileged elevation, matching every other write path’s UX, rather than splitting the operation across two separatepkexec/sudoprompts or redesigning the one-shot IPC protocol into a stateful one. Don’t take this as license to grow the ISO-parsing surface here further; it’s accepted specifically because the copy step cannot happen anywhere else without one of those two costs.crates/argos-session– UI-agnostic orchestration: resolving and refusing devices, classifying images, running the preflight checks, building thePlan, elevatingargos-helperand streaming its events. The seam between what Argos does and how a user is asked about it. It must not print, read stdin, or grow a terminal dependency (ctrlc,indicatif,consoleandclapall belong toargos-cli) – that is what lets a GUI reuse the exact same safety checks rather than reimplementing them.crates/argos-cli– theargoscommand-line tool.
Safety-critical code
Anything touching device selection or the write path exists to answer one
question: is it safe to write to this disk? Changes to
argos_core::device::Device::is_safe_to_write, the platform crates’
enumeration logic, or the privileged helper’s re-validation should:
- come with tests (unit tests for pure logic, the negative “never write a system disk” suite for anything device-selection related), and
- err on the side of refusing a device rather than guessing it’s safe.
argos-platform’s test-fixtures feature exposes a FakePlatform, the one
place PlatformOps is faked in this project. Its scope is deliberately
narrow: it exists to test orchestration – whether prepare_write runs its
checks in the right order, whether a front end copes with a device vanishing
mid-selection. It is not evidence about device safety. A fake that
declares a disk safe proves nothing about the code that decides that, so
anything touching enumeration or the safety gate stays tested against real
signals, real loop devices and real hdiutil images.
Commit messages
Commit messages are written in English, regardless of the language used in discussion or planning.
License
By contributing, you agree that your contributions will be licensed under either the MIT license or the Apache License 2.0, at the user’s option, as described in LICENSE-MIT and LICENSE-APACHE.
Changelog
All notable changes to Argos are documented here. Loosely follows Keep a Changelog.
[1.7.1] - 2026-10-09
Fixed
-
The macOS
.dmgcould never be granted Full Disk Access. Its universal binaries are built bylipo-ing an arm64 and an x86_64 build together, and only the arm64 half carried a signature: the linker ad-hoc signs arm64 output by itself, not x86_64. macOS validates every slice, soargos-helperfailed code validation and TCC denied Full Disk Access with the toggle switched on, surfacing as “opening the device: Operation not permitted” on every write and restore fromArgos.app. The app and its three binaries are now ad-hoc signed as a whole when the bundle is built. Local arm64-only builds were never affected, which is why 1.6.0’s validation, done on one, passed.Full Disk Access now goes to
Argos.appitself, added with the ordinary “+” button, rather than toargos-helperdragged out of the bundle: with the bundle signed, macOS attributes the helper’s disk access to the app it lives in. Upgrading from an earlier version: remove the old Argos and argos-helper entries from Full Disk Access and add/Applications/Argos.appagain. That re-grant is still needed after every update, because an ad-hoc signature changes with every build; the README walks through it.
[1.7.0] - 2026-10-08
Added
argos formatand “Restore drive…” in the GUI: turn an installer stick back into an ordinary one. A stick Argos wrote was hard to reuse with a desktop’s own formatter. Windows media carries a partition sized to its contents, and a Linux ISO written byte for byte carries the image’s own partition table (with a backup GPT where the image ended, not the device), so a file manager’s “Format” reformatted one small volume and left the rest of the stick unreachable – or refused the partition map outright. The new operation rewrites the partition table itself: it zeroes the first and last MiB of the device (old MBR and boot code, GPT headers, the ISO9660 descriptor that made Linux see the whole stick as a read-onlyiso9660), writes an MBR with one inactive FAT32 partition spanning the whole device, and formats it – the same pure-Rust, one-file-descriptor path as Windows media, so it works identically on Linux and macOS. Takes seconds. Same safety as a write: system disks are refused, the helper re-validates the device’s serial and size before touching it, and the device path has to be typed back to confirm. Devices from 512 MiB to 2 TiB;--label(or the field beside the GUI button) names the volume. Writing ISOs is unchanged.
[1.6.0] - 2026-09-09
Added
-
A graphical interface:
argos-gui. One window, in the spirit of Rufus – pick an image, pick a device, write. It shares every safety check with the CLI through a commonargos-sessioncrate (the same device refusals, the same TOCTOU re-validation in the privileged helper, the same retype-the- device-path confirmation), so there is no separate, weaker path through the window. Ships in the same package asargoson every platform – Homebrew, the.deb, the.dmg, and every release tarball install all three binaries (argos,argos-gui,argos-helper) together, never split – because a mismatched pair invites “the GUI can’t find its helper” as a confusing failure mode.Elevating from a window with no controlling terminal needed its own route on each host: macOS uses the system’s own authorization dialog (
osascript ... with administrator privileges) over a pair of FIFOs standing in for the pipes a terminal would have given it, so the password never transits the Argos process; Linux usespkexec, now with its own polkit policy (packaging/linux/org.argos.helper.policy) so the prompt names what is about to happen to the disk instead of showing polkit’s generic message. Both routes leaveargos-helperitself untouched.Cancelling is honest about what it can actually do: measured on real hardware, the
CancelTokenreaches the copy loop and nothing after it – not the final flush, not the read-back verification – so on a multi- gigabyte write the cancellable window is roughly the first 2% of the wall clock. Past that point the Cancel button goes inert and says why, rather than staying live through a press that would silently do nothing. -
The GUI speaks English and Brazilian Portuguese, in one binary, switchable without a restart. Detected from the desktop (
LC_ALL/LC_MESSAGES/LANGon Linux,AppleLanguageson macOS, becauseLANGis frequently empty for an app opened from Finder), overridable from a menu, and saved to a small config file ($XDG_CONFIG_HOME/argos/config.tomlon Linux,~/Library/Application Support/argos/config.tomlon macOS). Every label and every error message the window can show comes from a catalogue checked at compile time – a string missing from either language is a build failure, not a blank label found later. The CLI stays English-only: its help text, man page and shell completions all come from the sameclapdefinitions the packaging scripts run at build time, and a locale-dependentargos manwould ship whatever language the CI runner happened to have. -
Linux packaging now installs the GUI’s desktop integration. The
.deband the AUR package (once a tagged release containsargos-gui; see the AURPKGBUILD’s own comment) add a launcher entry, an icon under thehicolortheme, and the polkit policy above – so the graphical authorization prompt and the window itself are both reachable without a terminal. -
macOS packaging now produces a universal
Argos.appand.dmg, unsigned.lipo’d from both Apple Silicon and Intel builds, so nobody installing it has to know which one their Mac is. Deliberately not signed or notarized in this phase (a paid Developer ID plus notarization on every release, for a project whose macOS install story already has a better answer): Homebrew never sets the quarantine bit, sobrew installsidesteps Gatekeeper entirely. The.dmgis the convenience download for someone who wants the GUI without a Rust toolchain, withxattr -dr com.apple.quarantinedocumented for anyone who downloads it through a browser instead.
Fixed
-
argosnow exits with the code the privileged helper actually reported. Every failure that happened insideargos-helper– a device that turned out to be a system disk (12), a checksum mismatch after writing (17), a cancelled write (18) – exited 19 instead, because the unprivileged side wrapped the helper’s message in a generic I/O error and dropped theexit_codethe helper had gone to the trouble of sending. Anything scriptingargoscould tell “it failed” from “it worked” and nothing more. The messages themselves are unchanged; only the code is now the true one. Found while extractingargos-session. -
Dismissing the graphical authorization dialog now reports itself correctly. On Linux, cancelling the
pkexecprompt used to come back as a generic I/O failure (exit 19); it now readspkexec(1)’s own documented exit codes (126 for a dismissed dialog, 127 for any other authorization failure) and reports “authorization was declined” at exit 27, matching what a declined macOS prompt already said. Confirmed by hand, not guessed: exit 126 with an empty stderr andError executing command as another user: Request dismissedis what a real dismissal on GNOME actually produces. -
A translated helper error no longer glues the raw English message onto its own category, on the same un-collapsible line. Found on real hardware during G9’s validation: yanking a USB stick mid-write showed
operação cancelada: operation cancelled by user; the device is left in an inconsistent state...– a Portuguese phrase stitched to an entire English sentence. The raw text was never lost, only shown twice; it now stays in the “Details” pane, same as every other error category already did. -
The language selector reads as one now. The combo box in the header showed only its current value (“Automatic”, “English”…) with nothing beside it – a human tester read it as some kind of write/recording setting, not a language picker. It now carries a “Language”/“Idioma” label.
Changed
- The write phase now crosses the privilege boundary as a value rather than
as a
Debug-formatted string. No visible difference – every phase label a user has ever seen is byte-identical, and both binaries ship together in every package Argos produces – but a front end can now match on the phase to label it in the user’s own language, and renaming one is a compile error instead of a silently unlabelled progress bar. An older helper’s string form still parses, so a mixed pair degrades to an odd-looking label rather than a dropped event.
Known limitations
- On macOS, Full Disk Access does not survive an update.
argos-helperis unsigned and carries an ad-hoc code signature – a hash of its own contents – so every new build, including every tagged release, gets a new one. macOS’s TCC remembers a Full Disk Access grant by that identity, not by path, so upgradingArgos.apploses the grant along with the old binary: a write fails with a plainOperation not permittedagain until the newargos-helperis granted access by hand (System Settings -> Privacy & Security -> Full Disk Access), the same as on first install. Confirmed by reproducing it twice, independently, across real rebuilds. A stable signing identity would fix this; not done in this phase (seepackaging/README.md).
[1.5.5] - 2026-09-04
Changed
-
The write progress bar now tracks bytes the device has actually taken, not bytes merely handed to the OS – without slowing the write down. Reported from real hardware: the bar climbed to 100% in an instant (a
write()only queues bytes in the page cache), then the write sat there looking finished while the kernel was still flushing gigabytes to a slow USB stick in the background. 1.5.4’sFlushingphase named that wait but had nothing to show during it.Progress now comes from the kernel’s own per-device counter (field 7 of
/sys/block/<dev>/stat, completed sectors written), sampled during the copy and again on a background thread while the finalfsyncblocks – so the flush phase advances with real numbers instead of standing still. Nothing about the write path itself changed: it stays plain buffered writes, which is what lets the kernel merge them into large sequential device requests.The first attempt at this instead forced the answer to be true, with an
fsyncevery 64MiB. Measured on the same stick, that dropped a write the kernel otherwise kept 100% busy to roughly 1 MiB/s, because each barrier drains the queue and flushes the device’s internal cache. Watching a counter costs nothing; forcing a barrier costs almost everything.Platforms with no such counter (macOS, for now) fall back to the previous behavior rather than losing progress reporting.
Fixed
-
ejectfailing silently claimed “Safe to unplug” anyway. Reported from real hardware:eject: cannot open /dev/sdX: Permission deniedprinted to the terminal, immediately followed byEjected /dev/sdX. Safe to unplug.Both the Linux and macOSPlatformOps::ejectbackends discarded the underlyingeject/diskutil ejectexit status and always returnedOk(())–eject_best_effortin the CLI already had the right behavior for a real failure (a warning telling the user to eject manually, without failing the write), but that path was dead code since the backends never actually produced anErr. Both now report a real failure (while still treating a missingejectbinary on Linux as nothing to report, since there’s no eject-manage step to have failed); the write itself is unaffected either way – the physical flush already happened earlier, inargos-helper, before eject is ever attempted. -
The post-write eject then failed for a much simpler reason: it had no privilege. With the message above finally telling the truth, what it told was
could not eject /dev/sdg: eject exited with exit status: 1, fromeject: cannot open /dev/sdg: Permission denied– on a stock Ubuntu,/dev/sdXisroot:diskmode 0660 and the user runningargosis typically not indisk. Writing works because it happens in the elevatedargos-helper; ejecting was the one step left behind in the unprivileged CLI, so it could not even open the device it had just written. The eject now travels in the plan (WritePlan::eject,WriteWindowsPlan::eject) and runs in the helper, which reports the outcome back as a newEjectedevent – so the CLI prints the same two messages as before, but from a process that could actually do the work.--no-ejectis unchanged. Note this is anEACCESonopen, not theEBUSYan in-flight write would give: the data was already flushed and verified by the time eject ran.
[1.5.4] - 2026-09-04
Added
argos writenow also accepts--iso <path>, in addition to the existing positional form. Reported from real use, within hours of the README/man page documentingverify’s--iso: the same flag typed forwritewas a hard error, since its ISO had always been positional-only. Both forms parse to the same value; giving neither or both is a clear error rather than one being silently preferred. Positional remains what every example leads with – this only stops the other form from being a hard error.
Fixed
- The DD-mode write path never forced a real flush to the device.
Reported from real hardware: a 5.8GB Ubuntu ISO write to a USB stick
looked done almost instantly, then verification stalled with no visible
progress.
device.flush()is a no-op default onstd::fs::File, which doesn’t override it – a plainwrite()only queues bytes in the page cache, so the write loop finishing meant nothing about whether the OS had actually committed anything to the physical device yet. Verification’s read-back was landing behind whatever the kernel was still flushing in the background, which is what looked like a hang. Replaced with a realfsync(the samepartition_io::sync_devicethe Windows write path already used), and added aFlushingprogress phase so the wait is visible instead of looking like a stalled bar.
[1.5.3] - 2026-09-04
Changed
argos mannow documents every subcommand’s own flags on one page, instead ofclap_mangen’s default: a thin top-level page whose SUBCOMMANDS section cross-referencesargos-list(1),argos-write(1)and so on as if they were separate manual pages – which nothing in this project generates or installs, soman argos-writereported nothing found.list,write,verifyandcompletionseach get their own.SHblock (synopsis, description, options) inlined intoargos(1)instead, the waydd(1)/rsync(1)document a whole tool on one page. Also gives--help(both the top-level and per-subcommand) fuller prose, including an EXAMPLES block covering the same three-command flow as the README’s new Usage section.
Fixed
argos write/argos verifyfailed on a relative ISO path, elevating viapkexec. Reported from real hardware:argos write some.iso --device /dev/sdg, run from the directory holding the ISO, passed the confirmation prompt and started unmounting, then failed with a bare “No such file or directory” and no further explanation.pkexecresets the working directory before running the elevatedargos-helper(the same cwd-reset hardening any setuid-style launcher applies, so a relative path can’t resolve somewhere the caller didn’t intend) –sudocommonly preserves it, which is why this went unnoticed on macOS. The ISO path is now resolved to absolute inargositself, before the confirmation prompt, so a bad path fails immediately and by name instead of well after the user has already confirmed a write.
[1.5.2] - 2026-09-04
Also the first tag to actually exercise the .deb-building step
release.yml gained in this release – it was added and merged after
v1.5.1 had already been tagged, so v1.5.1’s GitHub Release went out built
by the old workflow and carries no .deb, despite the changelog below
describing it as done. This tag is what makes it real.
Added
- A Debian/Ubuntu
.debpackage (backlog #46).packaging/build-deb.shbuildsargosandargos-helpertogether, generates shell completions and the man page from the built binary (never a hand-maintained copy), and produces a package with a single dependency (libc6) – nomkfs.ntfs, nontfs-3g, nothing else. Built andlintian-checked on every push (CI) and attached to every GitHub Release. Install with:sudo apt install ./argos_<version>_amd64.deb. Seepackaging/README.mdfor how it’s built and two non-obvious thingscargo-debneeded correcting for. - A Homebrew tap (macOS):
brew install jp-guimaraes/argos/argos. Seejp-guimaraes/homebrew-argos. - An AUR
PKGBUILDfor pacman-based distros (Arch, and derivatives like Omarchy), same shape as the.deb: both binaries, completions and man page generated from the built binary, one runtime dependency (gcc-libs). Built andnamcap-checked on every push (CI). Not published to the AUR itself yet – that needs the maintainer’s own AUR account – so for now, build it locally frompackaging/aur/. Seepackaging/README.md.
Changed
- Release binaries are now stripped (
[profile.release] strip = true). Found missing while packaging the.deb: Debian’slintianflags an unstripped binary as a packaging error, andcargo-deb’s own stripping only takes effect when it does the build itself – a workspace build done ahead of time (as packaging, and cross-compiled release builds, both do) skipped it silently. Smaller binaries on every distribution channel as a side effect, not just the.deb. Note this doesn’t reach a plaincargo installfrom crates.io – that builds each crate outside the workspace, so the workspace-level profile setting doesn’t apply there.
[1.5.1] - 2026-09-04
Added
argos completions <shell>and a hiddenargos man(#46). Both are generated from the sameclapdefinition the CLI already carries, so a Homebrew formula, PKGBUILD or.debnever has to keep a hand-written copy in step with the real flags.argos manis hidden because its audience is packaging, not people at a terminal.
Changed
- Unmounting uses
umount2(2)instead of shelling out toumount(8)(M7.1, #46). One fewer runtime dependency for a distribution package to declare, and one fewer program whose presence or output could change under us. Note the argument changed with it:umount(8)accepts either the source device or the mountpoint, the syscall takes only the mountpoint.
Removed
- The
xtaskcrate. Its only task rebuiltcrates/argos-privileged/assets/uefi-ntfs.img, which was deleted along with the NTFS write path, and its only dependency existed to serve that task.
Fixed
- A USB stick whose label contains a space could not be unmounted.
/proc/mountsescapes space, tab, newline and backslash as\040,\011,\012and\134, and Argos read those fields verbatim – so a stick auto-mounted at/media/jp/My USBwas seen as/media/jp/My\040USB, a path that does not exist. Found while replacing theumountshell-out, which passes the mountpoint straight to the kernel and so has no tolerance for it.
[1.5.0] - 2026-09-03
Phase 3: self-contained Windows installer media, on Linux and macOS,
for UEFI and legacy BIOS machines. See
docs/plan-phase3-self-contained.md
for the plan this delivers against.
The headline: producing Windows install media no longer requires a Windows
machine, an external mkfs, a FUSE filesystem, or a vendored binary blob.
Added
-
image::udf— Argos’s own streaming UDF reader (M1, #40). Official Windows ISOs are UDF bridges, and the previous dependency read whole files into memory, which OOMed on a 5GBinstall.wim. Copying now runs in constant memory: 3 MiB peak RSS streaming a 7.58GB file, with a digest byte-identical to macOS’s own UDF driver reading the same file. -
image::wim— Argos’s own WIM reader and splitter (M2, #42). Splitsinstall.wimintoinstall.swmparts under FAT32’s 4GiB limit by redistributing whole stored resources verbatim: nothing is decompressed or re-encoded, so the lookup table’s SHA-1s stay valid by construction and no XPRESS/LZX codec is needed. Validated against wimlib as an external oracle (includingwimlib-imagex applyreproducing a source tree byte for byte from our parts) and, ultimately, by Windows Setup itself on real hardware. -
--layout fat32— a single pure-Rust FAT32 partition (M3, #43), written directly into the partition’s byte range of the open device: nomkfs, no mount, no partition device nodes, no kernel filesystem driver. GPT-partitioned; boots UEFI firmware via the ISO’s ownbootx64.efi. -
--layout fat32-bios— the same media, MBR-partitioned and carrying Argos’s own boot records (M6, #45): an MBR bootstrap in 279 of the 440 bytes available, and a FAT32 volume boot record in 418 of 420, both written from scratch in 16-bit assembly under MIT/Apache rather than portingms-sys’s GPL binary blobs. Boots legacy BIOS machines. -
Windows write support on macOS (M4, #34) — enabled by the FAT32 route, which superseded the original macFUSE/
ntfs-3gplan entirely. -
tools/mediadiff.py— dumps a written device’s full structure (MBR, GPT, BPB, FAT, directory tree) and diffs two dumps, with ~25 conformance checks. Written to run a differential diagnosis against Rufus-made media; it is what found the stale-GPT bug below. -
End-to-end write cancellation (M7.5, #35). Ctrl-C during a write now reaches the privileged helper:
argoskeeps the helper’s stdin open and aSIGINThandler writesprotocol::CANCEL_SIGNALinto it, a watcher thread in the helper turns that into aCancelToken, and the write path checks it on every buffer. The helper ignoresSIGINTitself – Ctrl-C reaches the whole foreground process group, and letting the default disposition kill the helper would stop it before it could act on the cancellation. The pipe closing counts as a cancellation too, so a parent that dies outright still stops the write.
Fixed
-
argos --versionreported0.1.0. The workspace version was never bumped, so v1.0.0 shipped — tagged, released and published — announcing itself as a pre-1.0 build. Set to1.5.0here, which is also the first version string the binary has ever agreed with the changelog on. -
A stale GPT surviving underneath the MBR layout (#59).
mbrmanwrites sector 0 and nothing else, so a stick recycled from--layout fat32kept its entire GPT — primary header, entry array, and backup header in the device’s last sector, CRCs intact — under a non-protective MBR. Windows refuses to assign a drive letter to a volume on a disk in that state, while the media still boots, which made it hard to localize. -
Directory entries
fatfswrites that violate the FAT specification: long filename entries placed before.and.., and..pointing at the root’s cluster instead of 0 (#56, confirmed upstream). -
BPB_HiddSecleft at 0 on the GPT path, and a BPB CHS geometry (32×64) that contradicted the 255×63 the MBR partition entries are built from. -
A fixed volume serial number, so every medium Argos wrote claimed the same identity; and a previous bootloader surviving in sector 0 across a GPT write.
-
Media writes now cost roughly 8× fewer I/O operations.
Removed
hadris-udf, and with it thecheck_windows_memorypreflight that existed only to refuse writes it would have OOMed on.- The NTFS/UEFI:NTFS Windows write path (
--layout ntfs), retired at decision point M4.3 once--layout fat32/fat32-bioswas validated on real hardware from both hosts and both firmwares (see below). Out of the tree, not just unused: the vendoreduefi-ntfs.imgboot image and its provenance doc, themkfs.ntfs/ntfs-3gshell-outs, the two-partitionWindowsPartitionPlan, and the threePlatformOpsmethods that existed only to support it (reread_partition_table,mount_ntfs_partition,unmount_path).--layoutnow defaults tofat32;ntfsis no longer a valid value.
Verified against real hardware
The acceptance criterion throughout was Windows Setup reaching disk
selection — not the language screen, which proves only that the firmware
found a bootloader. Reaching disk selection is what proves Setup found and
accepted its installation source, split .swm parts included.
- Media written from macOS: a UEFI machine (with an unsplit
install.esdand with a splitinstall.wim) and a legacy-BIOS machine (Intel Atom N455 netbook, AMI BIOS dated 2011), through Argos’s own MBR and VBR. - Media written from Linux (M5.1): a real Windows 10 22H2 ISO — 6.14 GB,
install.wim5.18 GB, so the splitter is genuinely exercised — to a physical 28.7 GB stick, booting a UEFI machine with--layout fat32and a legacy-BIOS machine with--layout fat32-bios. - Both hosts, both firmwares, with no Windows machine, no
mkfs.ntfs, nontfs-3g, no FUSE and no vendored binary blob anywhere in the path. - The stale-GPT scenario (#59) on real hardware: one stick written with the GPT layout and then the MBR layout still reached its installation source, which a surviving GPT would have prevented.
Still open: an installation carried through to completion rather than stopping at disk selection, and Secure Boot (M5.3).
Known limitations
- No GUI yet. Planned for 2.0; the CLI is architected so one can sit on the
same
argos-core/argos-platformcrates without a rewrite. - Windows as a host OS is still out of scope. Argos writes Windows media from Linux and macOS.
- Windows installer media has been carried to Setup’s disk selection on real hardware, on both firmwares and from both hosts, but not through a completed installation.
- Secure Boot is untested (M5.3).
- A solid
.esdlarger than FAT32’s 4GiB file limit cannot be written: only a WIM can be split, and a solid ESD cannot. Use an ISO edition whose install image is a WIM. - Non-hybrid ISOs are refused rather than partially supported.
- A Homebrew tap is not published yet (crates.io and GitHub Releases are both available today).
[1.0.0] - 2026-08-30
Initial release. Argos creates bootable Linux installer USB drives on
Linux and macOS hosts, in the spirit of
Rufus. See
docs/architecture.md for the full design.
Added
argos list— lists every disk the platform backend can see, and whether each looks safe to write to.argos write <iso> --device <id>— the core destructive flow: refreshes the target device, applies the non-negotiable safety gate (a system disk is refused unconditionally; a non-removable disk needs--i-know-what-im-doing), classifies the ISO (refusing anything that isn’t a hybrid image), runs capacity and source/target-collision preflight checks, requires the user to retype the exact device path to confirm, unmounts the device, writes it byte-for-byte (“DD mode”), verifies the write by reading it back and comparing hashes (--no-verifyto skip), ejects the device afterward (--no-ejectto skip), and reports progress as a live bar — or periodic plain-text lines when stdout isn’t a real terminal (a pipe, a log file, CI).argos verify <device> --iso <iso>— re-runs post-write verification against a device without writing again.- Safety model: a disk is only ever offered for writing when three independent signals agree — not a detected system disk, OS-reported removable, and on the USB bus. No single OS-reported flag is ever trusted alone, and the privileged helper re-checks identity immediately before opening the device regardless of what was confirmed earlier (the TOCTOU guard).
- Linux backend: disk enumeration via
/sys/block/*and the udev database, cross-checked against UDisks2 over D-Bus when reachable. System-disk detection resolves LVM/RAID/dm-crypt device-mapper stacks to the physical disk underneath. - macOS backend: disk enumeration via
diskutil list/info -plist, parsed defensively against schema drift across macOS versions. System-disk detection walks through APFS containers (the Apple Silicon case, where the boot volume’s immediate parent is a virtual container, not the real internal disk) to the physical disk underneath. - Privilege separation:
argos-helper, a small binary deliberately kept “dumb”, is the only thing that ever opens a raw device, and runs as root only for that. It parses no ISO and talks to no D-Bus/plist/UDisks2 API itself. - ISO classification: distinguishes hybrid ISOs (byte-for-byte writable) from El-Torito-only or plain-data images (refused) by inspecting the embedded MBR/GPT/El Torito structures already baked into the ISO — Argos never creates or modifies a partition table itself, only verifies one is already there before trusting a raw copy.
- Pre-built binaries on GitHub Releases for
x86_64-unknown-linux-gnu,aarch64-apple-darwin, andx86_64-apple-darwin. - Published on crates.io — install with
cargo install argos-cli argos-privileged(both crates are needed:argos-clialone installsargosbut not the separateargos-helperbinary it looks for next to itself at runtime).
Verified against real hardware
- Real, official Linux distro ISOs (Ubuntu 26.04.1, 22.04.5, and 18.04.5; Alpine Linux 3.24.1) written to real physical USB drives on both Linux and macOS hosts, checksum-verified against each distro’s own published sums before writing and independently re-hashed outside Argos afterward.
- Real boot confirmation on both UEFI and BIOS/legacy (MBR) hardware, including one genuinely old BIOS-only machine.
- A write to a real system disk is refused before any privilege
escalation, with and without
--i-know-what-im-doing. - Real-hardware integration tests for the write/verify path without
needing physical USB media for every run: Linux loop devices, macOS
hdiutil-attached disk images.
Known limitations
- Windows image support and Windows as a host OS are both out of scope for v1 — a future phase.
- No GUI yet; the CLI is architected so one can sit on top of the same
argos-core/argos-platformcrates later without a rewrite. - Non-hybrid ISOs are refused rather than partially supported.
- A Homebrew tap is not published yet (crates.io and GitHub Releases are both available today).
Download
The links below go straight to the files on GitHub — no need to browse the repository or its Releases page to find them.
Recommended per platform
- macOS: Homebrew (
brew install jp-guimaraes/argos/argos) is the primary install path and the only one Gatekeeper never blocks. The.dmgbelow is the convenience download for anyone who wants the GUI without a Homebrew/Rust setup. - Debian / Ubuntu: the
.debbelow (sudo apt install ./argos_<version>_amd64.deb). - Arch / pacman: not published to the AUR yet — see
packaging/README.mdfor building thePKGBUILDlocally. - Anything else: the plain tarball for your platform below.
Every method’s full instructions and caveats (Gatekeeper, Full Disk Access, PATH setup) are in Overview, under Installation.
Latest release
Everything else
- All releases — every past version, with full changelogs.
- Source code — to build it yourself, see Contributing.