isox 3.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
isox-3.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Logan Jackson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
isox-3.0.0/PKG-INFO ADDED
@@ -0,0 +1,387 @@
1
+ Metadata-Version: 2.4
2
+ Name: isox
3
+ Version: 3.0.0
4
+ Summary: Download Linux ISOs from the fastest mirror and verify them against published checksums
5
+ Author: Logan Jackson
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/logjxn/ISOx
8
+ Project-URL: Issues, https://github.com/logjxn/ISOx/issues
9
+ Keywords: linux,iso,download,checksum,mirror
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: System :: Archiving :: Mirroring
16
+ Classifier: Topic :: Utilities
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: requests>=2.34.2
21
+ Requires-Dist: beautifulsoup4>=4.15.0
22
+ Dynamic: license-file
23
+
24
+ # ISOx
25
+
26
+ ![Python](https://img.shields.io/badge/python-3.x-blue)
27
+ ![License](https://img.shields.io/github/license/logjxn/ISOx)
28
+ ![Release](https://img.shields.io/github/v/release/logjxn/ISOx)
29
+ ![OS](https://img.shields.io/badge/platform-Linux-orange)
30
+ ![Status](https://img.shields.io/badge/status-active-success)
31
+ ![CI](https://github.com/logjxn/ISOx/actions/workflows/ci.yml/badge.svg)
32
+
33
+ A command-line tool that downloads Linux distribution ISOs, races mirrors to find the fastest available source, and cryptographically verifies file integrity against the checksum published by the distribution itself, so you never have to manually hunt down hashes or skip verification because it's tedious.
34
+
35
+ ```
36
+ Select distro -> Compare mirror speeds -> Download .iso -> Verify checksum
37
+ (fastest mirror) (distro's own host)
38
+ ```
39
+
40
+ The ISO comes from whichever mirror is fastest right now. The checksum comes from the
41
+ distribution's own server, so the mirror that hands you the bytes is not also the one
42
+ vouching for them. See [What verification does and doesn't cover](#what-verification-does-and-doesnt-cover).
43
+
44
+ ## Why
45
+
46
+ I distro-hop a lot across laptops, tablets, Pis, and spare hardware. Manually visiting each project's download page, picking a mirror, and copy-pasting checksums to verify against every time got tedious enough that I started skipping the verification step entirely. This poses an integrity risk (modified ISOs, corruption, etc.), so I built a tool that automates the whole pipeline and makes verification the default, and not an extra step.
47
+
48
+ Furthermore, I simply love Linux. It's been my daily driver ever since I discovered it, and I want to see it continue to grow. I hope this tool makes getting started with Linux a little faster, easier, and safer for anyone who wants to use it.
49
+
50
+ ## Usage
51
+ List every supported distro:
52
+ ```bash
53
+ python isox.py --list
54
+ ```
55
+
56
+ Save somewhere other than `./ISOx_Downloads`:
57
+ ```bash
58
+ python isox.py arch --output-dir /mnt/usb
59
+ ```
60
+
61
+ Download and verify a distro:
62
+ ```bash
63
+ python isox.py arch
64
+ python isox.py debian
65
+ python isox.py kali
66
+ python isox.py alpine
67
+ python isox.py mint
68
+ python isox.py fedora
69
+ python isox.py opensuse
70
+ python isox.py gentoo
71
+ python isox.py void
72
+ python isox.py garuda
73
+ python isox.py ubuntu
74
+ python isox.py rocky
75
+ ```
76
+
77
+ Downloaded ISOs are saved to the created folder `ISOx_Downloads/`. Output looks like:
78
+
79
+ **A Normal Run**
80
+ ```
81
+ https://fastly.mirror.pkgbuild.com/iso/latest/archlinux-x86_64.iso sampled at 3.33 MB/s
82
+ https://geo.mirror.pkgbuild.com/iso/latest/archlinux-x86_64.iso sampled at 1.98 MB/s
83
+ https://mirror.rackspace.com/archlinux/iso/latest/archlinux-x86_64.iso sampled at 0.08 MB/s
84
+ Downloading archlinux-x86_64.iso from https://fastly.mirror.pkgbuild.com/iso/latest ...
85
+ [##############################] 100.0% 3.41 MB/s
86
+ Checksum matches, file is good.
87
+ ```
88
+ **Resumed Output**
89
+ ```
90
+ https://fastly.mirror.pkgbuild.com/iso/latest/archlinux-x86_64.iso sampled at 3.28 MB/s
91
+ https://geo.mirror.pkgbuild.com/iso/latest/archlinux-x86_64.iso sampled at 2.04 MB/s
92
+ https://mirror.rackspace.com/archlinux/iso/latest/archlinux-x86_64.iso is unreachable
93
+ Downloading archlinux-x86_64.iso from https://fastly.mirror.pkgbuild.com/iso/latest ...
94
+ Resuming from 743.6 MB ...
95
+ [##############################] 100.0% 3.26 MB/s
96
+ Checksum matches, file is good.
97
+ ```
98
+
99
+ ## Features
100
+
101
+ - **Config-driven distro support** - supported distros are defined in `distros.json`, not hardcoded, meaning adding a new distro is a JSON entry, not a code change.
102
+ - **Three ISO-discovery strategies** - covers distros that publish their ISOs in very different ways.
103
+ - **Checksums from the distro, ISO from the fastest mirror** - a mirror serving a modified ISO could serve a matching hash just as easily, so the hash is fetched from the distribution's own host rather than from whichever mirror won the speed race.
104
+ - **Version-folder auto-discovery** - for distros with no stable "latest" alias, the current version-numbered directory is discovered automatically by scanning a parent directory and numerically sorting version-like folder names, so outdated ISOs aren't retrieved.
105
+ - **Pre-release filtering** - distros publish release candidates into the same directory as final releases, and `_rc2` sorts *above* the release it precedes. Those are filtered out so `isox alpine` means the release, not the candidate.
106
+ - **No silent guessing** - if a distro's config matches more than one ISO in a checksum file, the run stops and names the candidates instead of picking one. A wrong image would still verify cleanly against its own published hash, so guessing is the one thing worse than failing.
107
+ - **Mirror speed checks** - samples ~2MB from each candidate mirror via a ranged request to measure real throughput, then downloads from the fastest.
108
+ - **Resumable downloads** - interrupted transfers are written to a `.part` file and continued via an HTTP `Range` request on the next run, so a drop at 90% doesn't cost you the 90% you already downloaded, even if a different mirror wins the race next time.
109
+ - **Stale-partial detection** - a `.part` left over from a *previous release* of a rolling distro is detected and discarded rather than merged into the new one.
110
+ - **Live progress bar** - shows percentage and real-time throughput, and degrades to a plain byte counter if the server won't report a total size.
111
+ - **Streamed downloads** - files are downloaded in large chunks (`requests` with `stream=True`) rather than loaded into memory all at once, so multi-GB ISOs don't hog RAM.
112
+ - **Checksum verification across three real-world formats** - the standard `<hash> <filename>` format, a single-hash-per-file format, and a BSD-style format are all normalized into the same lookup and compared with `hashlib`.
113
+ - **Multi-algorithm support** - uses `hashlib.new(algo)` rather than hardcoding a specific hash function, so the same code path supports SHA256, SHA512, or anything else `hashlib` supports.
114
+ - **Failure quarantine** - an ISO that fails verification is renamed rather than left in place, so it can't be mistaken for a verified file.
115
+ - **Single-point error handling** - every failure path exits with a one-line explanation and a non-zero status code, not a traceback.
116
+ - **Path-traversal protection** - filenames discovered from remote HTML listings are validated before ever being used in a URL or local file path.
117
+
118
+ ## How it works
119
+
120
+ ### Config format (`distros.json`)
121
+
122
+ Every distro entry needs `mirrors`, `checksum_filename`, and `hash_algo` at minimum. Everything else is optional and only needed if that distro deviates from the simplest cases such as Arch.
123
+
124
+ Fedora is shown as a more complex example on purpose. It demonstrates the additional options available when a distro needs version discovery, mirror scanning, or custom checksum handling. Most distributions only require the basic fields plus one or two optional ones.
125
+
126
+ If the included mirrors are not ideal for your location, you can easily update them. Just find a suitable mirror from the distro's official mirror list and replace the URL in distros.json. The tool will then handle the rest. Mirror, checksum and version-discovery URLs must be HTTPS. ISOx refuses a config with a plain-HTTP URL, but most distros have moved to HTTPS-only mirrors already, so this shouldn't narrow your options much.
127
+
128
+ ```json
129
+ {
130
+ "arch": {
131
+ "mirrors": ["https://fastly.mirror.pkgbuild.com/iso/latest/"],
132
+ "checksum_base": "https://geo.mirror.pkgbuild.com/iso/latest/",
133
+ "checksum_filename": "sha256sums.txt",
134
+ "hash_algo": "sha256",
135
+ "iso_filename": "archlinux-x86_64.iso"
136
+ },
137
+ "fedora": {
138
+ "mirrors": [
139
+ "https://dl.fedoraproject.org/pub/fedora/linux/releases/{version}/Workstation/x86_64/iso/",
140
+ "https://mirror.cs.princeton.edu/pub/mirrors/fedora/linux/releases/{version}/Workstation/x86_64/iso/",
141
+ "https://mirror.arizona.edu/fedora/linux/releases/{version}/Workstation/x86_64/iso/"
142
+ ],
143
+ "version_directory" : true,
144
+ "version_discovery_url" : [
145
+ "https://dl.fedoraproject.org/pub/fedora/linux/releases/",
146
+ "https://mirror.arizona.edu/fedora/linux/releases/"
147
+ ],
148
+ "checksum_base": "https://dl.fedoraproject.org/pub/fedora/linux/releases/{version}/Workstation/x86_64/iso/",
149
+ "checksum_filename" : "CHECKSUM",
150
+ "checksum_discovery_method" : "html_scan",
151
+ "checksum_format" : "bsd",
152
+ "discovery_method": "html_scan",
153
+ "hash_algo": "sha256",
154
+ "iso_filename_contains": ["Workstation", "x86_64"]
155
+ }
156
+ }
157
+ ```
158
+
159
+ | Key | Purpose |
160
+ |---|---|
161
+ | `mirrors` | **Required.** Where the ISO may be downloaded from. Raced on every run. |
162
+ | `checksum_filename` | **Required.** Literal name, or a `{iso_filename}.sha256` template. |
163
+ | `hash_algo` | **Required.** Anything `hashlib` supports. Validated before any download starts. |
164
+ | `checksum_base` | Host to fetch the checksum from, instead of the winning mirror. Should be the distro's own server. Also decides the ISO filename, so name and hash always agree. |
165
+ | `iso_filename` | For distros whose filename never changes. |
166
+ | `iso_filename_contains` | Substrings every candidate filename must contain. |
167
+ | `iso_filename_excludes` | Substrings that disqualify a filename, on top of the built-in pre-release filter. Use this when a distro publishes variants your substrings can't tell apart, like Debian's `-edu-` and `-mac-` images. |
168
+ | `discovery_method` | `checksum_scan` (default) or `html_scan`. |
169
+ | `checksum_discovery_method` | `html_scan` when the checksum filename itself has to be scraped. |
170
+ | `checksum_format` | `multi` (default), `bsd`, or `single`. |
171
+ | `version_directory` | `true` when the current version has to be discovered first. |
172
+ | `version_discovery_url` | One URL or a list of them, tried in order. |
173
+ | `version_scheme` | `ubuntu_lts` to select only LTS releases. |
174
+
175
+ #### Where `distros.json` is loaded from
176
+
177
+ Searched in this order, first hit wins:
178
+
179
+ 1. `$ISOX_DISTROS`, if set - this short-circuits the rest, so a typo is reported rather than silently falling back
180
+ 2. `~/.config/isox/distros.json` (`%APPDATA%\isox\distros.json` on Windows)
181
+ 3. Beside `isox.py` - the git clone case
182
+ 4. `share/isox/distros.json` under the install scheme's data directory, the user base, or `sys.prefix`
183
+
184
+ Four locations for the install case rather than one because a wheel's data files land
185
+ wherever the *install scheme* puts them, and that varies by how pip was invoked. venv and
186
+ pipx have `sys.prefix` and the data path coincide; `pip install --user` puts it under the
187
+ user base; Debian's patched system Python defaults to `/usr/local` while `sys.prefix` stays
188
+ `/usr`. Duplicates collapse, so a normal install shows one path, not three.
189
+
190
+ **If you customise mirrors on a pip install, put your copy at (2).** `pip install -U isox`
191
+ replaces what it installed, so edits made directly to (4) revert on upgrade without
192
+ warning. A copy in your config directory survives.
193
+
194
+ ```bash
195
+ mkdir -p ~/.config/isox
196
+ isox --list # prints the config path currently in use
197
+ cp "$(isox --list | sed -n 's/^config: //p')" ~/.config/isox/distros.json
198
+ ```
199
+
200
+ `isox --list` prints which file won, which is the quickest way to answer "why is it
201
+ using the wrong mirrors".
202
+
203
+ ### ISO filename discovery, three strategies
204
+
205
+ Not every distro publishes ISOs the same way, so the tool picks a strategy per distro based on which config fields are present. No per-distro code exists anywhere in the script.
206
+
207
+ - **`"iso_filename"`** - for distros with one fixed, unchanging filename.
208
+ - **`"iso_filename_contains"` + default discovery** - scans a shared checksum file for a filename matching all the given substrings, since versioned filenames would be inefficient to hardcode.
209
+ - **`"iso_filename_contains"` + `"discovery_method": "html_scan"`** - for distros with no single shared checksum file to scan. Scrapes the actual directory listing HTML with BeautifulSoup and filters `<a href>` links ending in `.iso` that match all the necessary substrings.
210
+
211
+ Both scanning strategies filter out pre-release artifacts by default. This matters more
212
+ than it sounds: Alpine publishes `alpine-extended-3.24.2_rc1-x86_64.iso` into the same
213
+ directory as the final release, and because `_` sorts above `-`, a plain "newest wins"
214
+ comparison hands you the release candidate. An RC has a perfectly valid published
215
+ checksum, so it would download and verify without complaint.
216
+
217
+ The two strategies also differ in how they break ties, deliberately. A directory listing
218
+ legitimately holds several versions, so the newest wins. A checksum file describes one
219
+ release, so several matches means the config is ambiguous rather than that there is a
220
+ choice to make - the run stops and names the candidates:
221
+
222
+ ```
223
+ Error: 'debian' matched 3 ISOs in the checksum file at
224
+ https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/SHA256SUMS:
225
+ debian-13.6.0-amd64-netinst.iso, debian-edu-13.6.0-amd64-netinst.iso,
226
+ debian-mac-13.6.0-amd64-netinst.iso. Narrow iso_filename_contains or add
227
+ iso_filename_excludes in distros.json.
228
+ ```
229
+
230
+ Only `.iso` files are considered. Checksum files cover every artifact of a release, and
231
+ Kali's `SHA256SUMS` pairs each ISO with a `.iso.torrent` that matches exactly the same
232
+ substrings - without the extension filter, a 100KB torrent could be downloaded, verified
233
+ against its own hash, and reported as a good ISO.
234
+
235
+ **Version-folder auto-discovery** (`"version_directory": true`) is a separate, earlier step for distros with no stable "latest" URL alias at all. Before any ISO discovery happens, the parent directory is scraped, version-numbered folder names are parsed and sorted *numerically*, and the newest one is spliced into every `{version}` placeholder across the mirror URLs.
236
+
237
+ `version_discovery_url` takes a list as well as a single URL, and they are tried in order.
238
+ This step decides which directory everything else is fetched from, so a single unreachable
239
+ host here would otherwise fail the whole run even when every configured mirror is healthy.
240
+
241
+ For Ubuntu, `version_scheme` is optional and only applies alongside version_directory. Left out, the newest version-numbered folder is selected. Set to "ubuntu_lts", only even-year .04 folders match, so `python isox.py ubuntu` resolves to the latest LTS. Interim releases are skipped intentionally, since LTS is the better default for a daily driver.
242
+
243
+ ### Checksum parsing
244
+
245
+ - **`"multi"` (default)** - the standard `<hash> <filename>` format used by `sha256sum`'s own output.
246
+ - **`"single"`** - the whole file content is treated as the hash, with the filename supplied from context rather than parsed. Available for distros that publish a genuinely bare hash.
247
+ - **`"bsd"`** - parses lines shaped like `SHA256 (filename) = hash`, used by some distros. Only lines starting with the configured `hash_algo` are read, so a file listing multiple algorithms for the same filename can't have the wrong one picked.
248
+
249
+ ### Mirror selection
250
+
251
+ Each candidate mirror is sampled with a ranged GET request, pulling the first ~2MB of the actual ISO via an HTTP `Range` header, and the real transfer speed (bytes/second) is measured over that sample. The mirror with the highest sampled throughput is selected for the ISO download. The checksum is fetched separately, from `checksum_base`.
252
+
253
+ Mirrors that time out or return an error status are caught (`requests.exceptions.RequestException`) and skipped rather than crashing the whole run.
254
+
255
+ **All Mirrors Down**
256
+ ```
257
+ https://fastly.mirror.pkgbuild.com/iso/latest/archlinux-x86_64.iso is unreachable
258
+ https://geo.mirror.pkgbuild.com/iso/latest/archlinux-x86_64.iso is unreachable
259
+ https://mirror.rackspace.com/archlinux/iso/latest/archlinux-x86_64.iso is unreachable
260
+ Error: none of the mirrors for this distro are reachable right now. Try again soon, or swap the mirrors in distros.json.
261
+ ```
262
+
263
+
264
+ ### Resumable downloads
265
+
266
+ Downloads are written to `<filename>.part` and only renamed to the final name once the transfer completes, so a partial file can never be mistaken for a finished one. On the next run, if a `.part` exists, its size becomes the offset in a `Range: bytes=N-` request and the transfer continues from there.
267
+
268
+ Four things can go wrong with a resume, and each is handled:
269
+
270
+ - **The `.part` is larger than the file on the server.** The server answers `416 Range Not Satisfiable`, which means the partial is stale: it's deleted and the download restarts.
271
+ - **The `.part` belongs to an older release.** Rolling distros like Arch reuse the same filename (`archlinux-x86_64.iso`) for a new image every month, so a partial from June would happily append onto July's file and produce a corrupt iso. ISOx stores the mirror's `ETag`/`Last-Modified` alongside the `.part` and discards the partial if it no longer matches. If the server publishes neither header, the partial is discarded after 24 hours instead.
272
+ - **A different mirror wins the race this time.** `ETag`s are per-server - nginx derives them from mtime and size, which differ between mirrors holding identical bytes - so a fingerprint from one mirror says nothing about another. The URL is recorded alongside the fingerprint and they're only compared when they came from the same place, otherwise the 24-hour age check applies. Without this, changing mirrors between runs looks exactly like the file changing, and a 90%-complete download gets thrown away.
273
+ - **The server ignores the `Range` header** and sends the whole file with a `200`. The offset is reset and the file is rewritten from scratch rather than appended to.
274
+
275
+ A transfer that ends short of the advertised `Content-Length` keeps its `.part` and exits
276
+ non-zero rather than being promoted. A short read is the exact case resuming exists for,
277
+ so turning it into a `.FAILED` ISO would throw away work that was still good.
278
+
279
+ **Stale Partial Downloads**
280
+ ```
281
+ Downloading archlinux-x86_64.iso from https://fastly.mirror.pkgbuild.com/iso/latest ...
282
+ Partial download doesn't match file on server. Starting fresh.
283
+ [##############################] 100.0% 3.39 MB/s
284
+ Checksum matches, file is good.
285
+ ```
286
+
287
+ Checksum verification remains the final stop: a bad merge that somehow slipped through would fail verification and be quarantined.
288
+
289
+ ### Checksum verification
290
+
291
+ The distro's checksum file is fetched new on every run, from `checksum_base` rather than from the mirror that served the ISO, and parsed into a `{filename: hash}` lookup dictionary. The downloaded file is then hashed via `hashlib` and compared against the expected hash with `hmac.compare_digest`, case-insensitively - `hexdigest()` is always lowercase and not every distro publishes it that way, and a case difference would otherwise be reported as tampering.
292
+
293
+ If the hashes don't match, the ISO is renamed to `<filename>.FAILED`. If no checksum entry could be found for the file at all, it's renamed to `<filename>.UNVERIFIED`. In both cases the process exits non-zero.
294
+
295
+ **Verification Failure**
296
+ ```
297
+ [##############################] 100.0% 3.12 MB/s
298
+ WARNING: checksum mismatch, file may be corrupted or tampered with!
299
+ Renamed to ISOx_Downloads/archlinux-x86_64.iso.FAILED so it can't be mistaken for a verified ISO.
300
+ ```
301
+
302
+ **This was tested:** I created a separate script to append garbage bytes to a previously-verified ISO, then ran the same `verify_checksum()` function used in the main program against it. It correctly returns `False`, confirming the verification logic detects tampering/corruption rather than always reporting success.
303
+ *The script I used to test corruption is below. Feel free to try for yourself.*
304
+ ```python
305
+ from isox import verify_checksum
306
+ import requests
307
+
308
+ response = requests.get("https://fastly.mirror.pkgbuild.com/iso/latest/sha256sums.txt")
309
+ hash_lookup = {}
310
+ for line in response.text.splitlines():
311
+ parts = line.split()
312
+ if len(parts) == 2:
313
+ hash_lookup[parts[1]] = parts[0]
314
+
315
+ result = verify_checksum("ISOx_Downloads/archlinux-x86_64.iso", "archlinux-x86_64.iso", hash_lookup, "sha256")
316
+ print("Verified:", result) # should print False now, after corruption
317
+ ```
318
+
319
+ ## What verification does and doesn't cover
320
+
321
+ **Covered.** Corruption in transit, truncated transfers, a bad disk, a botched resume, and
322
+ a mirror serving a modified ISO. That last one is why the checksum is fetched from
323
+ `checksum_base` - the distribution's own host - rather than from the mirror that served
324
+ the bytes. A mirror that can hand you a tampered ISO can hand you a hash matching it just
325
+ as easily, so verifying a mirror's file against that same mirror's hash is close to
326
+ verifying nothing. Splitting the two means one rogue mirror can't supply both halves.
327
+
328
+ **Not covered.** ISOx does not perform GPG signature verification, so it cannot prove the
329
+ checksum itself is authentic. If the distribution's own host is compromised, or someone
330
+ holds a certificate for it, a matching ISO and hash could be served together and ISOx
331
+ would report success.
332
+
333
+ GPG is not included as it would require maintaining trusted public keys (or fingerprints)
334
+ for every supported distribution, along with key management and signature validation
335
+ logic. That complexity conflicts with ISOx's goal of being a lightweight, easy to use, and
336
+ config-driven Linux tool.
337
+
338
+ Worth knowing: several distros already publish signed checksums that ISOx fetches and
339
+ parses while ignoring the signature - Gentoo's `.sha256` is PGP-clearsigned, and Rocky
340
+ ships a `CHECKSUM.asc` beside its `CHECKSUM`. If your threat model requires verifying the
341
+ origin of a release, those signatures are right there, and the distribution's
342
+ documentation covers its public signing keys and the verification steps.
343
+
344
+ ## Requirements
345
+
346
+ - Python 3.10+
347
+ - `requests`
348
+ - `beautifulsoup4` - used for HTML directory-listing discovery
349
+
350
+ Everything else (`hashlib`, `hmac`, `json`, `argparse`, `os`, `sys`, `time`, `re`) is part of the Python standard library.
351
+
352
+ Run it straight from a clone:
353
+
354
+ pip install -r requirements.txt
355
+ python isox.py arch
356
+
357
+ Or install it and get an `isox` command on your PATH:
358
+
359
+ pip install .
360
+ isox arch
361
+
362
+ ## Development
363
+
364
+ pip install -r requirements.txt -r requirements-dev.txt
365
+ pytest
366
+ black --check .
367
+ ruff check .
368
+ bandit isox.py
369
+
370
+ The suite is hermetic and stubs the network. `tests/test_live_mirrors.py` is the
371
+ one exception: it resolves every distro in `distros.json` against the real mirrors
372
+ and checks the filename it lands on has a published checksum, without downloading
373
+ any ISO. It's deselected by default and takes about 30 seconds:
374
+
375
+ pytest -m live # all 12 distros
376
+ pytest -m live -s # printing each resolved filename and hash
377
+ pytest -m live -k rocky
378
+
379
+ ## Contributing
380
+
381
+ Distro requests and additions are welcome - most new distros are a
382
+ `distros.json` entry with no Python at all. See
383
+ [CONTRIBUTING.md](https://github.com/logjxn/ISOx/blob/main/.github/CONTRIBUTING.md).
384
+
385
+ ## License
386
+
387
+ MIT License: see [LICENSE](https://github.com/logjxn/ISOx/blob/main/LICENSE) for details. Feel free to use, modify, or build on this.