Compare commits

35 Commits

Author SHA1 Message Date
Storm Dragon eb36d6d976 Fixed typo, or loss of current thought or something lol. 2026-05-24 22:19:14 -04:00
Storm Dragon 040eecca09 Build script compresses the image and generates the sha1sum now. 2026-05-24 21:27:07 -04:00
Storm Dragon 973b2573c8 Couple improvements to the install-stormux script. make sure hostname doesn't contain spaces. Try to make sure current information for mirrors even if current iso is a bit out of date. 2026-05-23 06:44:20 -04:00
Storm Dragon 9a0a6fef00 Update install-stormux script. 2026-05-11 21:34:43 -04:00
Storm Dragon 9496559875 A couple of management scripts added. 2026-04-03 07:27:38 -04:00
Storm Dragon ddab0d827f Update xlibre dummy with vt srcinfo 2026-04-02 19:28:40 -04:00
Storm Dragon aacdf8eb4a Rebase xlibre dummy with vt packaging 2026-04-02 19:28:36 -04:00
Storm Dragon a6c65ca973 Fix XLibre updater package order 2026-04-02 19:20:12 -04:00
Storm Dragon 19fd4b1ed4 Add XLibre updater design spec 2026-04-02 18:21:21 -04:00
Storm Dragon 185d098bdd First pass at custom mount options. Everything should be mounted on /mnt. 2026-02-26 14:35:13 -05:00
Storm Dragon 140a1c8f88 x86_64 iso file name more closely match the format of other Stormux images. Updated rhvoice fixes. 2025-12-21 15:09:57 -05:00
Storm Dragon 695b9e2f75 Added brltty udev.rules so it hopefully works more reliably. A few other minor tweaks. 2025-12-21 14:43:40 -05:00
Storm Dragon acf8327949 Implement sas for x86_64. 2025-12-20 23:52:16 -05:00
Storm Dragon 08123ea6e9 Initial implementation of Stormux Assistance Service. 2025-12-20 19:59:37 -05:00
Storm Dragon db6a4880b3 Added vi as alternate editor. 2025-12-20 06:25:23 -05:00
Storm Dragon 1a36316cfd Removed openssh for debuggin, finally remembered to re-add it. 2025-12-20 05:26:26 -05:00
Storm Dragon ccec75c4c5 Structural changes to the build script. Cleaner generation without having to edit an existing image. 2025-12-20 05:00:29 -05:00
Storm Dragon d8056278d2 Minor improvements to install-stormux. Added xdotool dependency for I38. 2025-12-19 02:56:48 -05:00
Storm Dragon 3d62262849 RHVoice fixes added. 2025-12-19 02:15:05 -05:00
Storm Dragon fbd1c3f671 A few stability improvements. Added Fenrir ssh announcement script. 2025-12-18 14:02:45 -05:00
Storm Dragon f4e91f7ee2 Updates to the x86_64 image creation. 2025-12-17 16:09:22 -05:00
Dane Stange 3f2dc0b492 README.md for the pi4 build-stormux.sh script 2025-12-01 12:41:56 -05:00
Storm Dragon aceda3f1f8 Add missing packages and a few other tweaks to installer. 2025-11-29 19:40:46 -05:00
Storm Dragon 013b3d11ac Moving toward working gui installation. 2025-11-29 03:54:28 -05:00
Storm Dragon dfd2609461 Basic installation now working, system comes up talking. 2025-11-25 18:07:51 -05:00
Storm Dragon b4704295ec Added missing package. 2025-11-25 01:34:11 -05:00
Storm Dragon 855be1afcd More work on the installer, not quite ready yet, but at a good save point. 2025-11-24 21:29:19 -05:00
Storm Dragon 3df5017766 Lots of improvements to the installer for x86_64. 2025-11-24 01:48:18 -05:00
Storm Dragon 34f1e24e10 Updated motd for both x86_64 and aarch64. 2025-11-23 13:21:57 -05:00
Storm Dragon 3176d951b6 Removed some unnecessary instructions from the first boot script. 2025-11-23 01:28:05 -05:00
Storm Dragon 370d351887 Trim down the configure-stormux stub a bit more. It's actually tiny now for what is needed. 2025-11-23 00:37:14 -05:00
Storm Dragon c31612ca51 Improve messages for x86_64, replace configure stormux with the installer. 2025-11-23 00:24:46 -05:00
Storm Dragon 1964121c8c Configure for xlibre, set up accessibility variables, update installation script. 2025-11-22 21:51:36 -05:00
Storm Dragon 68cbe3c0bb Updates to the x86_64 image build. 2025-11-20 15:32:22 -05:00
Storm Dragon a7e28c018f Gstreamer packages added for Fenrir sound support. 2025-11-20 15:29:21 -05:00
53 changed files with 6223 additions and 139 deletions
+7
View File
@@ -0,0 +1,7 @@
AGENTS.md
CLAUDE.md
*.qcow2
*.img
*.sha1sum
*.xz
*.zst
@@ -0,0 +1,57 @@
# XLibre Updater Design
## Goal
Add a repository-managed workflow for updating and building the Stormux XLibre package set in the correct dependency order, while keeping `xlibre-video-dummy-with-vt` manually maintained and reviewed against upstream `xlibre-video-dummy`.
## Scope
This design covers:
- correcting the package build order in `scripts/upgrade-xlibre.sh`
- reviewing and updating `scripts/xlibre-video-dummy-with-vt/PKGBUILD`
- regenerating `.SRCINFO` after PKGBUILD changes
This design does not automate AUR publication or rewrite the downstream package from upstream sources.
## Current State
The updater script currently builds packages in an order that does not match the XLibre dependency chain. `xlibre-video-dummy-with-vt` is also version-skewed relative to the current AUR `xlibre-video-dummy` package and needs a manual rebase of relevant packaging changes.
## Dependency Order
The package build order will be:
1. `xlibre-xserver-common`
2. `xlibre-xserver-devel`
3. `xlibre-input-libinput`
4. `xlibre-xserver`
5. `xlibre-video-fbdev`
6. `xlibre-video-dummy-with-vt`
This order reflects current AUR dependencies: `xlibre-xserver` requires `xlibre-xserver-common` and `xlibre-input-libinput`, while the input and video driver packages require `xlibre-xserver-devel` to build.
## Downstream Package Policy
`xlibre-video-dummy-with-vt` remains a separate, manually maintained package because it has a different maintainer and should not be auto-derived from the upstream AUR package.
The maintenance rule is:
- treat AUR `xlibre-video-dummy` as the packaging baseline
- manually port relevant upstream PKGBUILD changes into `xlibre-video-dummy-with-vt`
- keep only the intentional downstream delta needed for VT behavior
## Implementation Shape
`scripts/upgrade-xlibre.sh` will continue cloning packages from AUR and building them locally, but with the corrected order.
`scripts/xlibre-video-dummy-with-vt/PKGBUILD` will be updated to match the current upstream package structure where appropriate:
- current version and release
- current `depends` and `makedepends`
- current build flags and source layout
- regenerated checksums
The VT-specific patch remains the only behavioral divergence.
## Error Handling
The script should fail fast on clone or build errors and stop rather than continuing with a broken package chain. Because package order is intentional, partial success should be considered incomplete.
## Verification
Verification will be task-focused:
- confirm the updater script contains the corrected package order
- compare the downstream PKGBUILD against current upstream `xlibre-video-dummy`
- regenerate and verify `.SRCINFO`
- run `shellcheck` on the updater script
- run `makepkg --printsrcinfo` in the downstream package directory
+96
View File
@@ -0,0 +1,96 @@
# Stormux Pi Image Builder
Stormux provides an Arch-based Raspberry Pi image that already includes Fenrir, Orca, and other screen-reader friendly defaults. This repository hosts the scripts maintained by the Stormux community (originally crafted by Storm) so anyone can rebuild the image, customize the overlay, and share accessible Pi spins.
## Purpose & Scope
This repo captures the complete build pipeline for the Stormux Raspberry Pi images: downloading Arch Linux ARM, layering the accessible defaults, compiling Fenrir and helper tools, and producing a ready-to-flash `.img`. Running the script yourself lets you reproduce the same system the project ships for the Pi 4/400, tweak it, and publish new spins without reverse-engineering the original release.
## What You Need
- A 64-bit Arch Linux host (bare metal or VM) with at least 20GB free disk space.
- Root access on that host. The builder partitions loopback devices and must run as root.
- Required packages: `arch-install-scripts`, `dosfstools`, `parted`, `wget`, `qemu-user-static`, `qemu-user-static-binfmt`, plus standard developer tools that Arch already ships.
- A Raspberry Pi 4/400 or similar board (a Pi 3 works with the `-v 32` option) and a microSD card (8GB or larger recommended).
Install dependencies on the build host with:
```bash
sudo pacman -S arch-install-scripts dosfstools parted wget qemu-user-static qemu-user-static-binfmt
```
## Getting the Source
Clone the canonical repo (or your fork) someplace with plenty of space:
```bash
git clone https://git.stormux.org/storm/stormux.git
cd stormux
```
All build logic lives in `pi4/`. The `pi4/files/` tree mirrors the root filesystem you want inside the image (configs in `etc`, helper scripts in `usr/local/bin`, and so on).
### Where to Run the Builder
The script is designed for an x86_64 Arch host so it can emulate ARM binaries with QEMU while building. You can run it natively on an ARM board (including a Pi) running Arch or Stormux, but expect much longer build times and make sure the same packages are installed. Whatever the CPU, you **must** run as root and ensure `/mnt` is free so the loopback rootfs can mount safely.
## Building Your First Image
1. Become root (`sudo -i`) so the script can create loop devices and mount them.
2. From `/path/to/stormux`, run:
```bash
sudo ./pi4/build/build-stormux.sh -v 64 -l en_US -s 6
```
- `-v 64` builds the aarch64 image (use `-v 32` for the Pi 3/armv7h).
- `-l en_US` selects the locale (use `es_ES`, `fr_FR`, etc.).
- `-s 6` sets the image size in gigabytes.
- Add `-n my-stormux.img` to override the default filename.
3. Grab coffee. The script downloads the latest Arch Linux ARM tarball, provisions packages (Fenrir, Orca, NetworkManager, PipeWire, etc.), copies the overlay from `pi4/files/`, and cleans up.
4. When the `build-stormux.sh` command returns, you should have an `.img` file in your working directory (for example `stormux-pi4-aarch64-YYYY-MM-DD.img`).
If the build is interrupted, run `sudo umount -R /mnt && sudo losetup -D` to be sure nothing is still mounted.
## Customizing the Image
- **Overlay files**: Add or edit files in `pi4/files/` using the same paths they should have inside the Pi. Example: to add a custom MOTD, edit `pi4/files/etc/motd`.
- **Packages**: Edit the package list inside `pi4/build/build-stormux.sh` (search for `pacman -Su --needed`). Add or remove entries as needed, then rebuild.
- **AUR components**: The script already compiles Fenrir (`fenrir-git`), `growpartfs`, `log2ram`, and `yay`. To add more AUR packages, append them to the `aurPackages` array near the middle of the script.
- **First-boot behavior**: Modify `pi4/files/usr/local/bin/configure-stormux` to change the guided setup that runs when the image boots the first time.
Remember to keep permissions sensible (`chmod 755` for scripts, `644` for configs) so rsync copies them correctly.
## Keeping Up with Upstream Updates
Storm and other contributors actively improve these scripts. To sync your local clone with the latest changes:
```bash
cd /path/to/stormux
git pull --ff-only
```
Rebuild after pulling so your custom image inherits any upstream fixes (new packages, service tweaks, security patches). If you maintain your own fork, merge or rebase onto the upstream `main` branch before publishing updated images.
## Flashing and Booting
1. Verify the image:
```bash
ls -lh stormux-pi4-*.img
```
2. Write it to an SD card (replace `/dev/sdX` with your card, not a partition):
```bash
sudo dd if=stormux-pi4-aarch64-*.img of=/dev/sdX bs=4M status=progress conv=fsync
```
3. Insert the card into your Pi 4/400 and power it on. You should hear Fenrir start speaking once the user session loads.
4. Log in using the default credentials (username `stormux`, password `stormux`). Root uses `root`/`root` until you change it.
5. Run `sudo configure-stormux` (or follow the automatic prompt) to finish the guided setup: configure networking via `nmtui`, update the clock, optionally resize the SD card with `growpartfs`, and toggle Fenrirs layout.
## Testing Without Hardware
You can boot the image in a container to verify services before flashing:
```bash
sudo systemd-nspawn -i stormux-pi4-aarch64-*.img --boot
```
Use `machinectl` to connect to the console and ensure critical services start. For unit files you edit, run `systemd-analyze verify path/to/unit` inside the container or the Pi.
## Troubleshooting Tips
- **Locale or package errors**: Ensure your hosts pacman keyring is up to date (`sudo pacman -Sy archlinux-keyring`).
- **Loop device cleanup**: After failures run `sudo losetup -a` to find stray devices, then `sudo losetup -d /dev/loopX`.
- **Disk too small**: Increase the `-s` argument or remove packages.
- **Accessibility tweaks**: If Fenrir is too quiet or you prefer Orca, install the `orca` package inside the overlay and add it to the autostart configuration in `pi4/files/etc/xdg/autostart/`.
## Sharing Your Image
When you are happy with your customizations:
1. Rebuild so you have a clean `.img` file named clearly (for example, `stormux-pi4-a11y-v1.img`).
2. Compress it (`xz -T0 -z stormux-pi4-a11y-v1.img`).
3. Publish the image along with the exact commit you built from and any extra scripts you used. The README and `AGENTS.md` in this repo explain the layout so others can reproduce your work.
## Huge Thanks to Storm
Storm did the heavy lifting—curating packages, wiring up Fenrir/Orca defaults, scripting the first-boot assistant, and sharing the whole build process so the community can remix it. This repo exists because accessibility was treated as a first-class feature from day one, and the documentation lets anyone extend that work. If the image improves your Pi experience, consider contributing improvements or telling Storm thanks.
With these steps you can take the same base image Storm released, tweak it to your needs, and ship a fully accessible Arch image for other Pi fans. Happy hacking!
+177 -23
View File
@@ -24,16 +24,91 @@ mounted=1
set -e # Don't want to destroy stuff if this goes majorly wrong.
trap cleanup EXIT # make sure the script cleans up after itself before closing.
# shellcheck disable=SC2329 # verify_image is invoked from cleanup, which is invoked via trap EXIT
verify_image() {
echo "Checking completed image filesystems..."
fsck.vfat -n "${loopdev}p1"
e2fsck -fn "${loopdev}p2"
}
# shellcheck disable=SC2329 # compress_image is invoked from finish_build
compress_image() {
local compressedImage="${imageName}.xz"
if [[ ! -s "${imageName}" ]]; then
echo "Image file ${imageName} was not created or is empty."
return 1
fi
echo "Compressing ${imageName} to ${compressedImage}..."
xz -T0 -9 "${imageName}"
echo "Creating SHA-1 checksum for ${compressedImage}..."
sha1sum "${compressedImage}" > "${compressedImage}.sha1sum"
echo "Image build complete: ${compressedImage}"
echo "Checksum: ${compressedImage}.sha1sum"
}
# shellcheck disable=SC2329 # cleanup_image is invoked from cleanup and finish_build
cleanup_image() {
local verifyFilesystems="${1:-false}"
local cleanupStatus=0
cleanup() {
if [[ $mounted -eq 0 ]]; then
umount -R /mnt
partx -d "${loopdev}"
losetup --detach "${loopdev}"
if ! umount -R /mnt; then
cleanupStatus=1
elif [[ "$verifyFilesystems" == true ]]; then
if ! verify_image; then
cleanupStatus=1
fi
fi
fi
if [[ -n "${imageFileName}" ]]; then
rm "${imageFileName}"
if [[ -n "${loopdev:-}" ]]; then
if ! partx -d "${loopdev}"; then
cleanupStatus=1
fi
if ! losetup --detach "${loopdev}"; then
cleanupStatus=1
fi
fi
# Clean up temporary pacman config directory
if [[ -n "${tmpDir:-}" && -d "${tmpDir:-}" ]]; then
rm -rf "${tmpDir}"
fi
return "$cleanupStatus"
}
# shellcheck disable=SC2329 # cleanup is invoked via trap EXIT
cleanup() {
local status=$? # capture original exit status so failures propagate
local cleanupStatus=0
if ! cleanup_image false; then
cleanupStatus=1
fi
if [[ $status -eq 0 && $cleanupStatus -ne 0 ]]; then
echo "Image build commands completed, but cleanup or filesystem verification failed."
status=1
fi
exit "$status"
}
finish_build() {
trap - EXIT
if ! cleanup_image true; then
echo "Image build commands completed, but cleanup or filesystem verification failed."
exit 1
fi
if ! compress_image; then
exit 1
fi
exit 0
}
@@ -91,6 +166,12 @@ if [[ -e "$imageName" ]]; then
echo "${imageName} exists, please remove or move it for this script to continue."
exit 1
fi
for outputFile in "${imageName}.xz" "${imageName}.xz.sha1sum"; do
if [[ -e "$outputFile" ]]; then
echo "${outputFile} exists, please remove or move it for this script to continue."
exit 1
fi
done
# Make sure this script is ran as root.
if [ "$(whoami)" != "root" ] ; then
@@ -109,20 +190,23 @@ if [[ "$(uname -m)" == "x86_64" ]]; then
exit 1
fi
fi
for i in arch-install-scripts dosfstools parted wget ; do
for i in arch-install-scripts dosfstools parted ; do
if ! pacman -Q $i &> /dev/null ; then
echo "Please install $i before continuing."
exit 1
fi
done
for i in e2fsck fsck.vfat sha1sum xz ; do
if ! command -v "$i" &> /dev/null ; then
echo "Please install ${i} before continuing."
exit 1
fi
done
# Url for the aarch64 image to be downloaded.
imageUrl="http://os.archlinuxarm.org/os/ArchLinuxARM-rpi-aarch64-latest.tar.gz"
fallocate -l "$imageSize" "$imageName"
loopdev="$(losetup --find --show "${imageName}")"
parted --script "${loopdev}" mklabel msdos mkpart primary fat32 0% 200M mkpart primary ext4 200M 100%
parted --script "${loopdev}" mklabel msdos mkpart primary fat32 0% 512M mkpart primary ext4 512M 100%
mkfs.vfat -F32 -n STRMX_BOOT "${loopdev}p1"
mkfs.ext4 -F -L STRMX_ROOT "${loopdev}p2"
mount "${loopdev}p2" /mnt
@@ -130,9 +214,46 @@ mkdir /mnt/boot
mount "${loopdev}p1" /mnt/boot
# Things are mounted now, so set mounted to 0 (bash true)
mounted=0
imageFileName=$(mktemp)
wget "${imageUrl}" -O "${imageFileName}"
bsdtar -xpf "${imageFileName}" -C /mnt
# Set up temporary pacman configuration for ARM bootstrap
echo "Setting up pacman configuration for aarch64..."
tmpDir=$(mktemp -d)
mkdir -p "${tmpDir}/pacman.d"
# Create mirrorlist for Arch Linux ARM
cat > "${tmpDir}/pacman.d/mirrorlist" << 'MIRRORLIST'
Server = http://mirror.archlinuxarm.org/$arch/$repo
Server = http://fl.us.mirror.archlinuxarm.org/$arch/$repo
Server = http://il.us.mirror.archlinuxarm.org/$arch/$repo
Server = http://tx.us.mirror.archlinuxarm.org/$arch/$repo
Server = http://nj.us.mirror.archlinuxarm.org/$arch/$repo
MIRRORLIST
# Create pacman.conf for ARM
cat > "${tmpDir}/pacman.conf" << PACMANCONF
[options]
HoldPkg = pacman glibc
Architecture = aarch64
CheckSpace
SigLevel = Never
LocalFileSigLevel = Never
[core]
Include = ${tmpDir}/pacman.d/mirrorlist
[extra]
Include = ${tmpDir}/pacman.d/mirrorlist
[alarm]
Include = ${tmpDir}/pacman.d/mirrorlist
PACMANCONF
# Bootstrap the system with pacstrap
echo "Running pacstrap to install base system..."
pacstrap -c -G -M -C "${tmpDir}/pacman.conf" /mnt base base-devel linux-rpi raspberrypi-bootloader firmware-raspberrypi archlinuxarm-keyring
# Clean up temporary config
rm -rf "${tmpDir}"
# Copy override files into place before chroot (except skel to avoid conflicts)
cp -rv ../files/boot/* /mnt/boot
@@ -143,6 +264,9 @@ find ../files/etc -mindepth 1 -maxdepth 1 ! -name skel -exec cp -rv {} /mnt/etc/
PS1="(Chroot) [\u@\h \W] \$" arch-chroot /mnt << EOF
echo "Chroot started."
set -euo pipefail
# Disable pacman sandboxing (doesn't work in chroot)
sed -i '/^\[options\]/a DisableSandbox' /etc/pacman.conf
# set up pacman
pacman-key --init
pacman-key --populate archlinuxarm
@@ -194,9 +318,6 @@ else
fi
pacman -Syy
# Change to the Pi kernel for aarch64
pacman -R --noconfirm linux-aarch64 uboot-raspberrypi
pacman -S --noconfirm linux-rpi
# Install all packages (stormux repo has priority since it's listed first)
packages=(
@@ -209,15 +330,27 @@ packages=(
bluez-utils
brltty
cronie
dialog
espeak-ng
fake-hwclock
fenrir
firmware-raspberrypi
git
gstreamer
gst-plugins-base
gst-plugins-good
ii
# Keep Pi onboard firmware plus common USB/network chipset firmware without
# pulling in unrelated desktop/server GPU firmware.
linux-firmware-atheros
linux-firmware-broadcom
linux-firmware-mediatek
linux-firmware-realtek
magic-wormhole
man
man-pages
networkmanager
openssh
parted
pipewire
pipewire-alsa
@@ -225,9 +358,9 @@ packages=(
pipewire-pulse
poppler
python-dbus
python-gobject
python-pyenchant
python-pyte
python-setuptools-scm
python-pyperclip
raspberrypi-utils
socat
@@ -241,6 +374,7 @@ packages=(
wget
wireless-regdb
wireplumber
vi
xdg-user-dirs
xdg-utils
yay
@@ -248,6 +382,18 @@ packages=(
pacman -Su --needed --noconfirm "\${packages[@]}"
# Fix mkinitcpio preset for linux-rpi - kms hook fails on aarch64
# See: https://archlinuxarm.org/forum/viewtopic.php?t=16672
if [[ -f /etc/mkinitcpio.d/linux-rpi.preset ]]; then
echo "Configuring mkinitcpio preset to skip kms hook..."
sed -i "s/^default_options=.*/default_options=\"--skiphook kms\"/" /etc/mkinitcpio.d/linux-rpi.preset
sed -i "s/^fallback_options=.*/fallback_options=\"-S autodetect --skiphook kms\"/" /etc/mkinitcpio.d/linux-rpi.preset
fi
# Regenerate initramfs with the corrected settings
echo "Regenerating initramfs..."
mkinitcpio -P
# Restart gpg agents.
gpgconf --kill all
# set the language
@@ -260,8 +406,8 @@ systemctl enable rngd.service
# Set the distribution name.
echo 'Stormux \r (\l)' > /etc/issue
echo >> /etc/issue
# Change the alarm user to be stormux
usermod -a -g users -G wheel,realtime,audio,video,network,brlapi -m -d /home/stormux -l stormux alarm
# Create the stormux user
useradd -m -g users -G wheel,realtime,audio,video,network,brlapi -s /bin/bash stormux
# Grant sudo privileges to the stormux user for package installation.
echo 'stormux ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers.d/wheel
# Set the password for the root user
@@ -298,6 +444,8 @@ services=(
log-to-ram-sync.timer
log-to-ram-shutdown.service
NetworkManager.service
sshd.service
ssh-login-monitor.service
)
for service in "\${services[@]}"; do
@@ -308,13 +456,19 @@ for service in "\${services[@]}"; do
echo " \$service: FAILED"
fi
done
# Cleanup packages
pacman -Sc --noconfirm
# Re-enable pacman sandboxing for the final image
sed -i '/^DisableSandbox/d' /etc/pacman.conf
EOF
# Copy skel files to stormux user home (after user rename in chroot)
find ../files/etc/skel/ -mindepth 1 -exec cp -rv "{}" /mnt/home/stormux/ \;
# Exiting calls the cleanup function to unmount.
exit 0
# Copy boot files again to ensure custom config overrides any package changes
cp -rv ../files/boot/* /mnt/boot
# Clean up, verify, compress, and create the checksum.
finish_build
+198
View File
@@ -0,0 +1,198 @@
#! /bin/bash
#
# Copyright 2025, Stormux, <storm_dragon@stormux.org>
#
# This is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3, or (at your option) any later
# version.
#
# This software is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this package; see the file COPYING. If not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
#
set -euo pipefail
mountedPoints=()
loopDev=""
mountRoot=""
rootPart=""
bootPart=""
imagePath=""
cleanup() {
statusCode=$?
set +e
if [[ ${#mountedPoints[@]} -gt 0 ]]; then
for ((index=${#mountedPoints[@]}-1; index>=0; index--)); do
if mountpoint -q "${mountedPoints[$index]}"; then
umount "${mountedPoints[$index]}" || true
fi
done
fi
if [[ -n "${mountRoot}" && -d "${mountRoot}" && "${mountRoot}" != "/mnt" ]]; then
rmdir "${mountRoot}" 2>/dev/null || true
fi
if [[ -n "${loopDev}" ]]; then
partx -d "${loopDev}" 2>/dev/null || true
losetup --detach "${loopDev}" 2>/dev/null || true
fi
exit "$statusCode"
}
trap cleanup EXIT INT TERM
die() {
echo "Error: $*" >&2
exit 1
}
help() {
echo "Usage: $0 <image-file>"
echo "Mounts a Stormux image, chroots into it, and cleans up on exit."
exit 0
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
}
if [[ $# -ne 1 ]]; then
help
fi
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
help
fi
imagePath="$(readlink -f "$1" 2>/dev/null || true)"
if [[ -z "${imagePath}" ]]; then
die "Unable to resolve image path: $1"
fi
if [[ ! -f "${imagePath}" ]]; then
die "Image file not found: ${imagePath}"
fi
if [[ "$(whoami)" != "root" ]]; then
die "This script must be run as root."
fi
if command -v pacman >/dev/null 2>&1; then
for packageName in arch-install-scripts util-linux; do
if ! pacman -Q "$packageName" &>/dev/null; then
die "Please install ${packageName} before continuing."
fi
done
else
require_cmd arch-chroot
require_cmd losetup
require_cmd partx
require_cmd lsblk
require_cmd mount
require_cmd umount
require_cmd findmnt
require_cmd mountpoint
fi
if [[ "$(uname -m)" == "x86_64" ]]; then
if command -v pacman >/dev/null 2>&1; then
if ! pacman -Q qemu-user-static &>/dev/null; then
die "Please install qemu-user-static and qemu-user-static-binfmt before continuing."
fi
if ! pacman -Q qemu-user-static-binfmt &>/dev/null; then
die "Please install qemu-user-static and qemu-user-static-binfmt before continuing."
fi
else
require_cmd qemu-aarch64-static
fi
fi
require_cmd arch-chroot
require_cmd losetup
require_cmd partx
require_cmd lsblk
require_cmd mount
require_cmd umount
require_cmd findmnt
require_cmd mountpoint
baseMountDir="/mnt"
if [[ ! -d "${baseMountDir}" || ! -w "${baseMountDir}" ]]; then
die "/mnt is missing or not writable. Please create or fix permissions."
fi
if mountpoint -q "${baseMountDir}"; then
if [[ -t 0 ]]; then
echo "/mnt is already mounted. Unmount it to continue? [y/N]"
read -r answerText
case "${answerText}" in
y|Y|yes|YES)
umount "${baseMountDir}" || die "Failed to unmount /mnt"
;;
*)
die "Refusing to proceed while /mnt is mounted."
;;
esac
else
die "/mnt is already mounted. Refusing to proceed in non-interactive mode."
fi
fi
mountRoot="${baseMountDir}"
loopDev="$(losetup --find --show --partscan "${imagePath}")"
partx -u "${loopDev}" >/dev/null 2>&1 || true
declare -a rootCandidates=()
declare -a bootCandidates=()
while read -r deviceName fsType nodeType; do
if [[ "${nodeType}" != "part" ]]; then
continue
fi
case "${fsType}" in
ext4|ext3|ext2)
rootCandidates+=("${deviceName}")
;;
vfat|fat|fat32|fat16)
bootCandidates+=("${deviceName}")
;;
esac
done < <(lsblk -nrpo NAME,FSTYPE,TYPE "${loopDev}")
if [[ ${#rootCandidates[@]} -gt 0 ]]; then
rootPart="${rootCandidates[0]}"
elif [[ -b "${loopDev}p2" ]]; then
rootPart="${loopDev}p2"
else
die "Unable to locate root partition inside ${imagePath}"
fi
if [[ ${#bootCandidates[@]} -gt 0 ]]; then
bootPart="${bootCandidates[0]}"
elif [[ -b "${loopDev}p1" ]]; then
bootPart="${loopDev}p1"
fi
mount "${rootPart}" "${mountRoot}"
mountedPoints+=("${mountRoot}")
if [[ -n "${bootPart}" ]]; then
mkdir -p "${mountRoot}/boot"
mount "${bootPart}" "${mountRoot}/boot"
mountedPoints+=("${mountRoot}/boot")
fi
echo "Mounted image at ${mountRoot}. Entering chroot..."
PS1="(Chroot) [\\u@\\h \\W] \\$ " arch-chroot "${mountRoot}"
@@ -0,0 +1,4 @@
bifrost=buyfraust
danestange=dainstanggey
stange=stangey
kde=kaydeee
@@ -0,0 +1,19 @@
asus=aysus
certificate=cirtifficate
douche=doosh*
espeak=easpeak*
freenginx=freeenginex
git=ghit*
github=ghittehub
gitea=ghittee
jolla=yolla
jenux=jennux
lightnin=lighttnin
nginx=enginex
rhvoice=ahraychvoice
shit=shitt
sync=sink*
timezone=timezoan
vinux=vinnux
wench=wentch*
youngin=younggin*
+1 -1
View File
@@ -6,5 +6,5 @@ Welcome to Stormux, powered by Arch Linux ARM
Stormux IRC: #stormux on irc.stormux.org
Arch Linux ARM IRC: #archlinuxarm on irc.libera.chat
Thank you Stormux supporters! https://ko-fi.com/stormux/leaderboard
Thank you Stormux supporters! https://patreon.com/stormux
@@ -0,0 +1,17 @@
[Unit]
Description=Fenrir SSH Login Monitor
After=sshd.service
Wants=sshd.service
[Service]
Type=simple
ExecStart=/usr/share/fenrirscreenreader/scripts/ssh-login-monitor.sh
Restart=on-failure
RestartSec=5
# Security settings
NoNewPrivileges=true
PrivateTmp=false
[Install]
WantedBy=multi-user.target
@@ -0,0 +1 @@
SUBSYSTEM=="tty", KERNEL=="tty[0-9]*|hvc[0-9]*|sclp_line[0-9]*|ttysclp[0-9]*|3270/tty[0-9]*", GROUP="tty", MODE="0620"
+582
View File
@@ -0,0 +1,582 @@
#!/usr/bin/env python3
import os
import re
import secrets
import signal
import string
import subprocess
import sys
import tempfile
import time
import pwd
import threading
stormuxAdmin = ("storm",)
ircServer = "irc.stormux.org"
ircPort = 6667
ircChannel = "#stormux"
remoteHost = "billysballoons.com"
sasUser = "sas"
pingIntervalSeconds = 180
pingCount = 5
maxWormholeFailures = 3
sudoKeepaliveThread = None
sudoKeepaliveStop = threading.Event()
def speak_message(message):
try:
subprocess.run(["spd-say", message], check=False)
except FileNotFoundError:
print(message, flush=True)
def say_or_print(message, useSpeech):
if useSpeech:
speak_message(message)
else:
print(message, flush=True)
def run_command(command, inputText=None, check=False, env=None):
return subprocess.run(
command,
input=inputText,
text=True,
capture_output=True,
check=check,
env=env,
)
def ensure_sudo(useSpeech):
if os.geteuid() == 0:
return True
if useSpeech:
speak_message("Sudo password required. Please enter your password now.")
result = run_command(["sudo", "-v"])
if result.returncode == 0:
start_sudo_keepalive()
return True
return False
def start_sudo_keepalive():
global sudoKeepaliveThread
if sudoKeepaliveThread and sudoKeepaliveThread.is_alive():
return
def keepalive_loop():
while not sudoKeepaliveStop.wait(240):
run_command(["sudo", "-n", "-v"])
sudoKeepaliveThread = threading.Thread(target=keepalive_loop, daemon=True)
sudoKeepaliveThread.start()
def run_privileged(command, useSpeech, inputText=None, check=True):
if os.geteuid() == 0:
fullCommand = command
else:
if not ensure_sudo(useSpeech):
raise RuntimeError("sudo authentication failed")
fullCommand = ["sudo"] + command
return run_command(fullCommand, inputText=inputText, check=check)
def run_as_user(userName, command, useSpeech, check=True):
if os.geteuid() == 0:
fullCommand = ["sudo", "-u", userName, "-H"] + command
else:
if not ensure_sudo(useSpeech):
raise RuntimeError("sudo authentication failed")
fullCommand = ["sudo", "-u", userName, "-H"] + command
return run_command(fullCommand, check=check)
def user_exists(userName):
result = run_command(["getent", "passwd", userName])
return result.returncode == 0
def ensure_wheel(userName, useSpeech):
result = run_command(["id", "-nG", userName])
groups = result.stdout.strip().split()
if "wheel" not in groups:
run_privileged(["usermod", "-a", "-G", "wheel", userName], useSpeech)
def generate_password():
allowedChars = string.ascii_letters + string.digits
length = secrets.randbelow(5) + 6
return "".join(secrets.choice(allowedChars) for _ in range(length))
def get_user_home(userName):
return pwd.getpwnam(userName).pw_dir
def set_password(userName, password, useSpeech):
run_privileged(["chpasswd"], useSpeech, inputText=f"{userName}:{password}\n")
def generate_ssh_key(userName, useSpeech):
userHome = get_user_home(userName)
sshDir = os.path.join(userHome, ".ssh")
privateKeyPath = os.path.join(sshDir, "id_ed25519")
publicKeyPath = f"{privateKeyPath}.pub"
run_privileged(["mkdir", "-p", sshDir], useSpeech)
run_privileged(["chown", f"{userName}:{userName}", sshDir], useSpeech)
run_privileged(["chmod", "700", sshDir], useSpeech)
for entry in list_subdirs(sshDir):
if os.path.isfile(entry) or os.path.islink(entry):
run_privileged(["rm", "-f", entry], useSpeech, check=False)
run_as_user(
userName,
["ssh-keygen", "-t", "ed25519", "-N", "", "-f", privateKeyPath],
useSpeech,
)
run_privileged(["chmod", "600", privateKeyPath], useSpeech)
run_privileged(["chmod", "644", publicKeyPath], useSpeech)
run_privileged(["chown", f"{userName}:{userName}", privateKeyPath, publicKeyPath], useSpeech)
return privateKeyPath, publicKeyPath
def path_exists_for_user(path, userName, useSpeech):
result = run_as_user(userName, ["stat", path], useSpeech, check=False)
return result.returncode == 0
def extract_message_text(line):
if "> " in line:
return line.split("> ", 1)[1].strip()
if ": " in line:
return line.split(": ", 1)[1].strip()
return line.strip()
def parse_sender(line):
match = re.search(r"<([^>]+)>", line)
if match:
return match.group(1)
return None
def find_wormhole_code(message):
lowered = message.strip().lower()
if lowered in ("yes", "accept"):
return None
match = re.search(r"\b\d+-[A-Za-z0-9-]+\b", message)
if match:
return match.group(0)
return None
class IrcSession:
def __init__(self, server, port, nick, channel, baseDir):
self.server = server
self.port = port
self.nick = nick
self.channel = channel
self.baseDir = baseDir
self.serverDir = None
self.serverInPath = None
self.channelInPath = None
self.iiProcess = None
self.pmOffsets = {}
def start(self):
if not shutil_which("ii"):
raise RuntimeError("ii is not installed")
supportsI = ii_supports_i()
processEnv = os.environ.copy()
iiCommand = ["ii", "-s", self.server, "-p", str(self.port), "-n", self.nick]
if supportsI:
iiCommand += ["-i", self.baseDir]
else:
processEnv["HOME"] = self.baseDir
self.iiProcess = subprocess.Popen(
iiCommand,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=processEnv,
)
self.serverDir = self.wait_for_server_dir()
self.serverInPath = os.path.join(self.serverDir, "in")
def stop(self):
if self.channelInPath and os.path.exists(self.channelInPath):
try:
self.write_line(self.channelInPath, f"/part {self.channel}")
except OSError:
pass
if self.serverInPath and os.path.exists(self.serverInPath):
try:
self.write_line(self.serverInPath, "/quit")
except OSError:
pass
if self.iiProcess and self.iiProcess.poll() is None:
self.iiProcess.terminate()
try:
self.iiProcess.wait(timeout=5)
except subprocess.TimeoutExpired:
self.iiProcess.kill()
def join_channel(self):
joinMessage = f"/join {self.channel}"
channelDir = os.path.join(self.serverDir, self.channel)
channelAltDir = os.path.join(self.serverDir, self.channel.lstrip("#"))
startTime = time.monotonic()
nextJoinTime = startTime
while time.monotonic() - startTime < 60:
if time.monotonic() >= nextJoinTime:
self.write_line(self.serverInPath, joinMessage)
nextJoinTime = time.monotonic() + 5
for candidate in (channelDir, channelAltDir):
inPath = os.path.join(candidate, "in")
if os.path.exists(inPath):
self.channelInPath = inPath
return
time.sleep(0.5)
self.channelInPath = None
def send_channel_message(self, message):
if not self.channelInPath:
self.refresh_channel_in_path()
if self.channelInPath and os.path.exists(self.channelInPath):
self.write_line(self.channelInPath, message)
else:
self.write_line(self.serverInPath, f"/msg {self.channel} {message}")
def send_private_message(self, nick, message):
nickDir = os.path.join(self.serverDir, nick)
inPath = os.path.join(nickDir, "in")
if os.path.exists(inPath):
self.write_line(inPath, message)
else:
self.write_line(self.serverInPath, f"/msg {nick} {message}")
def get_private_messages(self, allowedUsers):
messages = []
for nick in allowedUsers:
nickDir = os.path.join(self.serverDir, nick)
outPath = os.path.join(nickDir, "out")
if not os.path.exists(outPath):
continue
lastPos = self.pmOffsets.get(outPath, 0)
with open(outPath, "r", encoding="utf-8", errors="ignore") as fileHandle:
fileHandle.seek(lastPos)
for line in fileHandle:
sender = parse_sender(line)
if sender and sender == self.nick:
continue
if sender and sender != nick:
continue
messageText = extract_message_text(line)
if messageText:
messages.append((nick, messageText))
self.pmOffsets[outPath] = fileHandle.tell()
return messages
def refresh_channel_in_path(self):
channelDir = os.path.join(self.serverDir, self.channel)
channelAltDir = os.path.join(self.serverDir, self.channel.lstrip("#"))
for candidate in (channelDir, channelAltDir):
inPath = os.path.join(candidate, "in")
if os.path.exists(inPath):
self.channelInPath = inPath
return
def wait_for_server_dir(self):
for _ in range(120):
for rootDir in [self.baseDir] + list_subdirs(self.baseDir):
if not os.path.isdir(rootDir):
continue
for entry in os.listdir(rootDir):
path = os.path.join(rootDir, entry)
if os.path.isdir(path) and self.server in entry:
inPath = os.path.join(path, "in")
if os.path.exists(inPath):
return path
time.sleep(0.5)
raise RuntimeError("ii server directory not found")
@staticmethod
def write_line(path, message):
with open(path, "w", encoding="utf-8", errors="ignore") as fileHandle:
fileHandle.write(message + "\n")
fileHandle.flush()
def ii_supports_i():
result = run_command(["ii", "-h"])
output = (result.stdout or "") + (result.stderr or "")
return "-i" in output
def list_subdirs(path):
try:
return [os.path.join(path, entry) for entry in os.listdir(path)]
except OSError:
return []
def shutil_which(command):
for path in os.environ.get("PATH", "").split(os.pathsep):
candidate = os.path.join(path, command)
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
return candidate
return None
def build_nick():
baseUser = os.environ.get("SUDO_USER") or os.environ.get("USER") or "sas"
return f"{baseUser}-{int(time.time())}"
def main():
say_or_print("Checking accessibility. Is your screen reader working? (y/n)", True)
answer = input().strip().lower()
useSpeech = answer in ("n", "no")
shouldRemoveUser = False
cleanupDone = False
tempDir = tempfile.mkdtemp(prefix="sas-ii-")
ircSession = None
sshProcess = None
def cleanup(exitMessage=None):
nonlocal cleanupDone
if cleanupDone:
return
cleanupDone = True
nonlocal sshProcess
if exitMessage:
say_or_print(exitMessage, useSpeech)
if sshProcess and sshProcess.poll() is None:
sshProcess.terminate()
try:
sshProcess.wait(timeout=5)
except subprocess.TimeoutExpired:
sshProcess.kill()
if ircSession:
ircSession.stop()
if shouldRemoveUser:
try:
run_privileged(["pkill", "-u", sasUser], useSpeech, check=False)
time.sleep(1)
result = run_privileged(["userdel", "-r", sasUser], useSpeech, check=False)
run_privileged(["rm", "-rf", f"/home/{sasUser}"], useSpeech, check=False)
if result.returncode != 0 and user_exists(sasUser):
say_or_print(
"Cleanup warning: failed to remove sas user. Please remove it manually.",
useSpeech,
)
except Exception:
pass
sudoKeepaliveStop.set()
if sudoKeepaliveThread:
sudoKeepaliveThread.join(timeout=2)
try:
remove_tree(tempDir)
except Exception:
pass
def handle_signal(signum, frame):
cleanup("Interrupted. Cleaning up.")
sys.exit(1)
signal.signal(signal.SIGINT, handle_signal)
signal.signal(signal.SIGTERM, handle_signal)
try:
if not user_exists(sasUser):
run_privileged(
["useradd", "-m", "-d", f"/home/{sasUser}", "-s", "/bin/bash", "-G", "wheel", sasUser],
useSpeech,
)
shouldRemoveUser = True
else:
say_or_print(
"User 'sas' exists. Remove and recreate it? This will delete /home/sas. (y/n)",
useSpeech,
)
response = input().strip().lower()
if response not in ("y", "yes"):
cleanup("The sas user is unavailable. Remove it manually and try again.")
return 1
run_privileged(["pkill", "-u", sasUser], useSpeech, check=False)
run_privileged(["userdel", "-r", sasUser], useSpeech, check=False)
run_privileged(["rm", "-rf", f"/home/{sasUser}"], useSpeech, check=False)
run_privileged(
["useradd", "-m", "-d", f"/home/{sasUser}", "-s", "/bin/bash", "-G", "wheel", sasUser],
useSpeech,
)
shouldRemoveUser = True
ensure_wheel(sasUser, useSpeech)
password = generate_password()
set_password(sasUser, password, useSpeech)
privateKeyPath, publicKeyPath = generate_ssh_key(sasUser, useSpeech)
sasHome = get_user_home(sasUser)
knownHostsPath = os.path.join(sasHome, ".ssh", "known_hosts_sas")
run_privileged(["touch", knownHostsPath], useSpeech)
run_privileged(["chmod", "600", knownHostsPath], useSpeech)
run_privileged(["chown", f"{sasUser}:{sasUser}", knownHostsPath], useSpeech)
nick = build_nick()
ircSession = IrcSession(ircServer, ircPort, nick, ircChannel, tempDir)
ircSession.start()
ircSession.join_channel()
say_or_print("Waiting for assistance on IRC.", useSpeech)
startTime = time.monotonic()
nextPingTime = startTime
pingsSent = 0
confirmedAdmin = None
while time.monotonic() - startTime < pingIntervalSeconds * pingCount:
now = time.monotonic()
if pingsSent < pingCount and now >= nextPingTime:
ircSession.send_channel_message(f"{nick} is requesting assistance.")
pingsSent += 1
nextPingTime = startTime + (pingsSent * pingIntervalSeconds)
for adminNick, messageText in ircSession.get_private_messages(stormuxAdmin):
if messageText.strip().lower() in ("yes", "accept"):
confirmedAdmin = adminNick
break
if confirmedAdmin:
break
time.sleep(1)
if not confirmedAdmin:
cleanup("No one was available to help, please try again later.")
return 1
ircSession.send_private_message(
confirmedAdmin,
f'password: "{password}" please send wormhole ssh invite code',
)
failures = 0
while failures < maxWormholeFailures:
inviteCode = None
while inviteCode is None:
for adminNick, messageText in ircSession.get_private_messages(stormuxAdmin):
inviteCode = find_wormhole_code(messageText)
if inviteCode:
break
if inviteCode:
break
time.sleep(1)
if not path_exists_for_user(publicKeyPath, sasUser, useSpeech):
raise RuntimeError(f"Public key missing: {publicKeyPath}")
wormholeCommand = [
"wormhole",
"ssh",
"accept",
"--yes",
inviteCode,
]
result = run_as_user(sasUser, wormholeCommand, useSpeech, check=False)
if result.returncode == 0:
say_or_print("Wormhole key transfer succeeded.", useSpeech)
break
failures += 1
errorTextFull = (result.stderr or result.stdout or "").strip()
if errorTextFull and not useSpeech:
print("Wormhole ssh accept error:", flush=True)
print(errorTextFull, flush=True)
errorText = errorTextFull
if errorText:
errorText = " ".join(errorText.split())
if len(errorText) > 400:
errorText = errorText[:400] + "..."
ircSession.send_private_message(
confirmedAdmin,
f"Wormhole ssh accept failed: {errorText}",
)
ircSession.send_private_message(
confirmedAdmin,
"Wormhole ssh accept failed. Please send a new invite code.",
)
if failures >= maxWormholeFailures:
cleanup("Wormhole failed too many times. Exiting.")
return 1
say_or_print("Starting reverse SSH tunnel. Press Ctrl+C to stop.", useSpeech)
sshCommand = [
"ssh",
"-N",
"-R",
"localhost:2232:localhost:22",
"-o",
"ExitOnForwardFailure=yes",
"-o",
"BatchMode=yes",
"-o",
"ServerAliveInterval=30",
"-o",
"ServerAliveCountMax=3",
"-o",
"StrictHostKeyChecking=accept-new",
"-o",
f"UserKnownHostsFile={knownHostsPath}",
"-i",
privateKeyPath,
f"{sasUser}@{remoteHost}",
]
sshCommand = ["sudo", "-u", sasUser, "-H"] + sshCommand
sshProcess = subprocess.Popen(sshCommand)
sshProcess.wait()
except Exception as exc:
cleanup(f"Error: {exc}")
return 1
finally:
cleanup()
return 0
def remove_tree(path):
if not os.path.exists(path):
return
for rootDir, dirNames, fileNames in os.walk(path, topdown=False):
for fileName in fileNames:
try:
os.unlink(os.path.join(rootDir, fileName))
except OSError:
pass
for dirName in dirNames:
try:
os.rmdir(os.path.join(rootDir, dirName))
except OSError:
pass
try:
os.rmdir(path)
except OSError:
pass
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,185 @@
#!/usr/bin/env bash
# SSH Login Monitor for Fenrir Screen Reader
# Monitors SSH logins and announces them via Fenrir's speech system
# Configuration
fenrirSocket="/tmp/fenrirscreenreader-deamon.sock"
logFile="/var/log/auth.log"
stateFile="/tmp/fenrir-ssh-monitor.state"
checkInterval=2 # seconds between checks
# Voice settings
announceUser=true
announceIp=true
announceHostname=true
announceLogout=false # Announce SSH disconnections (disabled by default - may not work reliably on all systems)
# Function to send message to Fenrir
fenrirSay() {
local message="$1"
# Only announce if Fenrir socket exists (silently skip if not)
if [[ -S "$fenrirSocket" ]]; then
echo "command say ${message}" | socat - UNIX-CLIENT:"${fenrirSocket}" 2>/dev/null
fi
}
# Function to get last processed line number
getLastLine() {
if [[ -f "$stateFile" ]]; then
cat "$stateFile"
else
echo "0"
fi
}
# Function to save last processed line number
saveLastLine() {
echo "$1" > "$stateFile"
}
# Function to parse SSH login and announce
processLogin() {
local logLine="$1"
local user=""
local ip=""
local hostname=""
# Parse different SSH login patterns
# Pattern 1: "Accepted publickey for USER from IP"
# Pattern 2: "Accepted password for USER from IP"
if [[ "$logLine" =~ Accepted\ (publickey|password|keyboard-interactive/pam)\ for\ ([^[:space:]]+)\ from\ ([^[:space:]]+) ]]; then
user="${BASH_REMATCH[2]}"
ip="${BASH_REMATCH[3]}"
# Try to resolve hostname
if command -v host &> /dev/null && [[ "$announceHostname" == "true" ]]; then
hostname="$(host "$ip" 2>/dev/null | grep -oP 'domain name pointer \K[^.]+' | head -1)"
fi
# Build announcement message (concise format)
local message=""
if [[ "$announceUser" == "true" ]]; then
message+="${user} "
fi
message+="S S H login"
if [[ "$announceIp" == "true" ]]; then
message+=" from ${ip}"
fi
if [[ -n "$hostname" ]] && [[ "$announceHostname" == "true" ]]; then
message+=" ${hostname}"
fi
fenrirSay "$message"
return 0
fi
return 1
}
# Function to parse SSH logout and announce
processLogout() {
local logLine="$1"
local user=""
# Parse SSH disconnect patterns
# Pattern: "pam_unix(sshd:session): session closed for user USER"
if [[ "$logLine" =~ session\ closed\ for\ user\ ([^[:space:]]+) ]]; then
user="${BASH_REMATCH[1]}"
if [[ "$announceLogout" == "true" ]]; then
local message=""
if [[ "$announceUser" == "true" ]]; then
message+="${user} "
fi
message+="disconnected from S S H"
fenrirSay "$message"
fi
return 0
fi
return 1
}
# Function to monitor auth.log
monitorAuthLog() {
local lastLine
lastLine=$(getLastLine)
# Get total lines in log
local totalLines
totalLines=$(wc -l < "$logFile" 2>/dev/null || echo "0")
# If log was rotated, reset
if [[ $totalLines -lt $lastLine ]]; then
lastLine=0
fi
# Process new lines
if [[ $totalLines -gt $lastLine ]]; then
local newLines=$((totalLines - lastLine))
# Read only new lines
tail -n "$newLines" "$logFile" 2>/dev/null | while IFS= read -r line; do
if [[ "$line" =~ sshd.*Accepted ]]; then
processLogin "$line"
elif [[ "$line" =~ sshd.*session\ closed ]]; then
processLogout "$line"
fi
done
saveLastLine "$totalLines"
fi
}
# Function to monitor journalctl (alternative for systemd systems)
monitorJournalctl() {
# Follow journalctl for SSH logins and logouts
journalctl -u sshd -u ssh -f -n 0 --no-pager 2>/dev/null | while IFS= read -r line; do
if [[ "$line" =~ Accepted ]]; then
processLogin "$line"
elif [[ "$line" =~ session\ closed ]]; then
processLogout "$line"
fi
done
}
# Check if running as root
if [[ "$(id -u)" -ne 0 ]]; then
echo "Error: This script must be run with sudo privileges to access system logs."
exit 1
fi
# Note: We don't require Fenrir to be running at startup
# The script will silently skip announcements when Fenrir socket doesn't exist
# Determine monitoring method
if command -v journalctl &> /dev/null && systemctl is-active --quiet sshd 2>/dev/null; then
echo "Starting SSH login monitor (using journalctl)..."
fenrirSay "SSH login monitor started."
# Use journalctl for real-time monitoring
trap 'fenrirSay "SSH login monitor stopped."; exit 0' INT TERM
monitorJournalctl
elif [[ -f "$logFile" ]]; then
echo "Starting SSH login monitor (using auth.log)..."
fenrirSay "SSH login monitor started."
# Use auth.log polling
trap 'fenrirSay "SSH login monitor stopped."; rm -f "$stateFile"; exit 0' INT TERM
while true; do
monitorAuthLog
sleep "$checkInterval"
done
else
echo "Error: Cannot find SSH logs. Neither journalctl nor ${logFile} is available."
exit 1
fi
+194
View File
@@ -0,0 +1,194 @@
#!/usr/bin/env bash
set -euo pipefail
shopt -s nullglob
# ---- Configurable Variables ----
repoDir="/var/www/packages.stormux.org"
keyId="52ADA49000F1FF0456F8AEEFB4CDE1CD56EF8E82"
repoName="stormux"
dbName="${repoName}.db.tar.gz"
filesName="${repoName}.files.tar.gz"
rebuildDb="${REBUILD_DB:-false}"
require_cmd() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "❌ Required command not found: $cmd"
exit 1
fi
}
# ---- Safety Checks ----
if [[ ! -d "$repoDir" ]]; then
echo "❌ Repo dir does not exist: $repoDir"
exit 1
fi
require_cmd "gpg"
require_cmd "repo-add"
require_cmd "repo-remove"
# ---- Create Architecture Directories ----
mkdir -p "$repoDir/x86_64" "$repoDir/aarch64"
# ---- Process Each Architecture ----
process_arch() {
local arch="$1"
local archDir="$repoDir/$arch"
local pkgFiles=()
local selectedPkgFiles=()
local repoAddArgs=()
local -A currentPkgNames=()
local -A newestPkgByName=()
local repoPkgNames=()
local dbFile="$dbName"
local filesFile="$filesName"
local dbSig="${dbFile}.sig"
local filesSig="${filesFile}.sig"
local dbLink="${repoName}.db"
local filesLink="${repoName}.files"
echo "🏗️ Processing $arch packages..."
# Enter arch directory (packages should already be sorted by update.sh)
cd "$archDir" || return 1
# Select only the newest archive for each package name. repo-add cannot
# safely consume multiple versions of the same package in a single run.
pkgFiles=( *.pkg.tar.zst *.pkg.tar.xz )
for pkg in "${pkgFiles[@]}"; do
local pkgName pkgVersion existingPkg existingVersion
pkgName="$(bsdtar -xOqf "$pkg" .PKGINFO | sed -n 's/^pkgname = //p' | head -n1)"
pkgVersion="$(bsdtar -xOqf "$pkg" .PKGINFO | sed -n 's/^pkgver = //p' | head -n1)-$(bsdtar -xOqf "$pkg" .PKGINFO | sed -n 's/^pkgrel = //p' | head -n1)"
if [[ -z "$pkgName" || -z "$pkgVersion" ]]; then
echo "❌ Unable to determine package metadata for $pkg"
cd "$repoDir" || exit 1
return 1
fi
existingPkg="${currentPkgNames[$pkgName]:-}"
if [[ -z "$existingPkg" ]]; then
currentPkgNames["$pkgName"]="$pkg"
else
existingVersion="$(bsdtar -xOqf "$existingPkg" .PKGINFO | sed -n 's/^pkgver = //p' | head -n1)-$(bsdtar -xOqf "$existingPkg" .PKGINFO | sed -n 's/^pkgrel = //p' | head -n1)"
if (( $(vercmp "$pkgVersion" "$existingVersion") > 0 )); then
currentPkgNames["$pkgName"]="$pkg"
fi
fi
done
for pkgName in "${!currentPkgNames[@]}"; do
newestPkgByName["$pkgName"]="${currentPkgNames[$pkgName]}"
selectedPkgFiles+=( "${currentPkgNames[$pkgName]}" )
done
pkgFiles=( "${selectedPkgFiles[@]}" )
# Sign all unsigned selected packages
echo "🔏 Signing $arch packages..."
for pkg in "${pkgFiles[@]}"; do
if [[ ! -f "$pkg.sig" ]]; then
echo " 📝 Signing $pkg"
gpg --default-key "$keyId" --detach-sign "$pkg"
else
echo "$pkg already signed"
fi
done
# Track which package names should remain in the repo after this run.
for pkg in "${pkgFiles[@]}"; do
local pkgName
pkgName="$(bsdtar -xOqf "$pkg" .PKGINFO | sed -n 's/^pkgname = //p' | head -n1)"
if [[ -n "$pkgName" ]]; then
currentPkgNames["$pkgName"]=1
else
echo "❌ Unable to determine package name for $pkg"
cd "$repoDir" || exit 1
return 1
fi
done
# Rebuild database for this architecture
if [[ "$rebuildDb" == "true" ]]; then
echo "🗃️ Rebuilding $arch repo database..."
rm -f "$dbFile" "$dbSig" "$filesFile" "$filesSig" "$dbLink" "$filesLink"
elif [[ -f "$dbFile" ]]; then
echo "🗃️ Updating $arch repo database..."
else
echo "🆕 Creating new $arch repo database..."
fi
# Remove packages that still exist in the repo database but are no longer
# present in the current directory. repo-remove with --remove also deletes
# the matching package archive and detached signature from disk.
if [[ -f "$dbFile" ]]; then
mapfile -t repoPkgNames < <(
bsdtar -tf "$dbFile" |
awk -F/ 'NF == 2 && $2 == "desc" {print $1}' |
while read -r entry; do
bsdtar -xOf "$dbFile" "${entry}/desc" |
awk 'found {print; exit} /^%NAME%$/ {found=1}'
done |
sort -u
)
for pkgName in "${repoPkgNames[@]}"; do
if [[ -z "${currentPkgNames[$pkgName]:-}" ]]; then
echo "🧹 Removing stale repo package $pkgName"
repo-remove --sign --key "$keyId" --remove "$dbFile" "$pkgName"
fi
done
fi
# Only run repo-add if there are packages
if ((${#pkgFiles[@]} > 0)); then
repoAddArgs=(--sign --key "$keyId" --remove)
if [[ "$rebuildDb" != "true" && -f "$dbFile" ]]; then
repoAddArgs+=(--verify)
fi
repo-add "${repoAddArgs[@]}" "$dbFile" "${pkgFiles[@]}"
if [[ ! -f "$dbFile" ]]; then
echo "❌ repo-add did not create $dbFile"
cd "$repoDir" || exit 1
return 1
fi
if [[ ! -e "$dbLink" ]]; then
ln -s "$dbFile" "$dbLink"
fi
if [[ -f "$filesFile" && ! -e "$filesLink" ]]; then
ln -s "$filesFile" "$filesLink"
fi
echo "$arch repo updated successfully"
else
echo "️ No $arch packages found"
fi
# Remove orphaned package archives and detached signatures that are not part
# of the current package set. This keeps the on-disk repo contents aligned
# with the package database after rebuilds and package removals.
for pkg in *.pkg.tar.zst *.pkg.tar.xz; do
local pkgName
pkgName="$(bsdtar -xOqf "$pkg" .PKGINFO | sed -n 's/^pkgname = //p' | head -n1)"
if [[ -z "${newestPkgByName[$pkgName]:-}" || "${newestPkgByName[$pkgName]}" != "$pkg" ]]; then
echo "🧹 Removing orphaned package file $pkg"
rm -f "$pkg" "$pkg.sig"
fi
done
cd "$repoDir" || exit
}
# ---- Process Both Architectures ----
process_arch "x86_64"
process_arch "aarch64"
# ---- Done ----
echo "✅ All repositories updated and signed successfully."
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
startDir="$(pwd)"
buildDir="${startDir}/xlibre-build"
packageList=(
xlibre-input-libinput
xlibre-xserver
xlibre-video-amdgpu
xlibre-video-ati
xlibre-video-fbdev
xlibre-video-intel
xlibre-video-nouveau
xlibre-video-vesa
xlibre-video-dummy-with-vt
)
mkdir -p "${buildDir}"
for i in "${packageList[@]}" ; do
yay -Ga "$i"
pushd "$i"
makepkg -Acrsf
cp -v ./*.pkg.tar.* "${buildDir}/"
popd
done
@@ -0,0 +1,23 @@
pkgbase = xlibre-video-dummy-with-vt
pkgdesc = XLibre dummy video driver with an allocated vt
pkgver = 25.0.0
pkgrel = 5
url = https://github.com/X11Libre/xf86-video-dummy
arch = x86_64
arch = aarch64
groups = xlibre-drivers
license = MIT
license = X11
makedepends = xlibre-xserver-devel>=25.0
makedepends = xorgproto
depends = xlibre-xserver>=25.0
depends = glibc
provides = xf86-video-dummy
provides = x11win-video-dummy
conflicts = xf86-video-dummy
source = https://github.com/X11Libre/xf86-video-dummy/archive/refs/tags/xlibre-xf86-video-dummy-25.0.0.tar.gz
source = dummy_driver.patch
sha256sums = b56e610705cd3d4d86422a11c6b0d93357e4d4749a05178a85fd250301d357b9
sha256sums = 68cdcf21e9b54a7fdb8e968292e1ef9ad154ddb1361b141a0a635c2a13c92bfa
pkgname = xlibre-video-dummy-with-vt
@@ -0,0 +1,5 @@
*
!PKGBUILD
!.SRCINFO
!.gitignore
!.nvchecker.toml
@@ -0,0 +1,5 @@
[xlibre-video-dummy]
source = "git"
git = "https://github.com/x11libre/xf86-video-dummy.git"
include_regex = "xlibre-xf86-video-dummy-.*"
prefix = "xlibre-xf86-video-dummy-"
@@ -0,0 +1,59 @@
# Maintainer: Storm Dragon <storm_dragon@linux-a11y.org>
pkgname=xlibre-video-dummy-with-vt
pkgver=25.0.0
pkgrel=5
pkgdesc="XLibre dummy video driver with an allocated vt"
arch=(x86_64 aarch64)
_pkgname=xf86-video-dummy
url="https://github.com/X11Libre/${_pkgname}"
license=('MIT' 'X11')
depends=("xlibre-xserver>=${pkgver%.*}" 'glibc')
makedepends=("xlibre-xserver-devel>=${pkgver%.*}" 'xorgproto')
conflicts=("${_pkgname}")
provides=("${_pkgname}" 'x11win-video-dummy')
source=("${url}/archive/refs/tags/xlibre-${_pkgname}-${pkgver}.tar.gz"
"dummy_driver.patch")
groups=('xlibre-drivers')
sha256sums=('b56e610705cd3d4d86422a11c6b0d93357e4d4749a05178a85fd250301d357b9'
'68cdcf21e9b54a7fdb8e968292e1ef9ad154ddb1361b141a0a635c2a13c92bfa')
prepare() {
cd "${srcdir}/${_pkgname}-xlibre-${_pkgname}-${pkgver}/src"
patch -i "${srcdir}/dummy_driver.patch"
}
build() {
case "$CARCH" in
"x86_64")
CFLAGS=" -march=x86-64"
;;
"aarch64")
CFLAGS=" -march=armv8-a"
;;
*)
CFLAGS=" -march=native"
;;
esac
CFLAGS+=" -mtune=generic -O2 -pipe -fexceptions -Wp,-D_FORTIFY_SOURCE=3 -Wformat -Werror=format-security"
CFLAGS+=" -fstack-clash-protection -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer"
LDFLAGS=" -Wl,-O1 -Wl,--sort-common -Wl,--as-needed -Wl,-z,lazy -Wl,-z,relro -Wl,-z,pack-relative-relocs"
if [[ $CARCH != 'aarch64' ]]; then
CFLAGS+=" -fcf-protection"
fi
CXXFLAGS="${CFLAGS} -Wp,-D_GLIBCXX_ASSERTIONS"
export CFLAGS="${CFLAGS}"
export CXXFLAGS="${CXXFLAGS}"
export LDFLAGS="${LDFLAGS}"
cd "${srcdir}/${_pkgname}-xlibre-${_pkgname}-${pkgver}"
./autogen.sh
./configure --prefix=/usr
make
}
package() {
cd "${srcdir}/${_pkgname}-xlibre-${_pkgname}-${pkgver}"
make DESTDIR="${pkgdir}" install
install -Dm644 COPYING "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
}
@@ -0,0 +1,17 @@
--- a/src/dummy_driver.c
+++ b/src/dummy_driver.c
@@ -1016,10 +1016,10 @@ dummyDriverFunc(ScrnInfoPtr pScrn, xorgDriverFuncOp op, pointer ptr)
CARD32 *flag;
switch (op) {
- case GET_REQUIRED_HW_INTERFACES:
- flag = (CARD32*)ptr;
- (*flag) = HW_SKIP_CONSOLE;
- return TRUE;
+ case GET_REQUIRED_HW_INTERFACES:
+ flag = (CARD32*)ptr;
+ /* Allow the driver to allocate a VT instead of skipping the console. */
+ return TRUE;
default:
return FALSE;
}
@@ -0,0 +1,4 @@
bifrost=buyfraust
danestange=dainstanggey
stange=stangey
kde=kaydeee
@@ -0,0 +1,19 @@
asus=aysus
certificate=cirtifficate
douche=doosh*
espeak=easpeak*
freenginx=freeenginex
git=ghit*
github=ghittehub
gitea=ghittee
jolla=yolla
jenux=jennux
lightnin=lighttnin
nginx=enginex
rhvoice=ahraychvoice
shit=shitt
sync=sink*
timezone=timezoan
vinux=vinnux
wench=wentch*
youngin=younggin*
+3 -4
View File
@@ -1,10 +1,9 @@
Welcome to Stormux, powered by Arch Linux ARM
Welcome to Stormux, powered by Arch Linux
Stormux Website: https://stormux.org
Arch Linux ARM Forum: https://archlinuxarm.org/forum
Arch Linux: https://archlinux.org
Stormux IRC: #stormux on irc.stormux.org
Arch Linux ARM IRC: #archlinuxarm on irc.libera.chat
Thank you Stormux supporters! https://ko-fi.com/stormux/leaderboard
Thank you Stormux supporters! https://patreon.com/stormux
+2 -1
View File
@@ -25,7 +25,8 @@ Architecture = auto
#IgnorePkg =
#IgnoreGroup =
#NoUpgrade =
# Protect Stormux custom skel files from being overwritten by package updates
NoUpgrade = etc/skel/.bashrc etc/skel/.inputrc etc/skel/.screenrc etc/skel/.vimrc etc/skel/.vim/*
#NoExtract =
# Misc options
+961
View File
@@ -0,0 +1,961 @@
##
## Arch Linux repository mirrorlist
## Generated on 2025-10-21
##
## Worldwide
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch
Server = https://mirror.rackspace.com/archlinux/$repo/os/$arch
#Server = https://fastly.mirror.pkgbuild.com/$repo/os/$arch
#Server = https://ftpmirror.infania.net/mirror/archlinux/$repo/os/$arch
#Server = http://mirror.rackspace.com/archlinux/$repo/os/$arch
## Albania
#Server = https://al.arch.niranjan.co/$repo/os/$arch
## Armenia
#Server = http://mirrors.teamcloud.am/archlinux/$repo/os/$arch
#Server = https://mirrors.teamcloud.am/archlinux/$repo/os/$arch
## Australia
#Server = https://mirror.aarnet.edu.au/pub/archlinux/$repo/os/$arch
#Server = http://au.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://au.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch
#Server = https://archlinux.mirror.digitalpacific.com.au/$repo/os/$arch
#Server = http://gsl-syd.mm.fcix.net/archlinux/$repo/os/$arch
#Server = https://gsl-syd.mm.fcix.net/archlinux/$repo/os/$arch
#Server = https://sydney.mirror.pkgbuild.com/$repo/os/$arch
#Server = http://ftp.iinet.net.au/pub/archlinux/$repo/os/$arch
#Server = http://mirror.internode.on.net/pub/archlinux/$repo/os/$arch
#Server = https://au.arch.niranjan.co/$repo/os/$arch
#Server = http://syd.mirror.rackspace.com/archlinux/$repo/os/$arch
#Server = https://syd.mirror.rackspace.com/archlinux/$repo/os/$arch
#Server = http://ftp.swin.edu.au/archlinux/$repo/os/$arch
## Austria
#Server = http://mirror.alwyzon.net/archlinux/$repo/os/$arch
#Server = https://mirror.alwyzon.net/archlinux/$repo/os/$arch
#Server = http://at.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://at.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://mirror.digitalnova.at/archlinux/$repo/os/$arch
#Server = http://mirror.easyname.at/archlinux/$repo/os/$arch
#Server = https://at.arch.mirror.kescher.at/$repo/os/$arch
#Server = https://at.arch.niranjan.co/$repo/os/$arch
#Server = https://at-vie.soulharsh007.dev/archlinux/$repo/os/$arch
## Azerbaijan
#Server = http://mirror.ourhost.az/archlinux/$repo/os/$arch
#Server = https://mirror.ourhost.az/archlinux/$repo/os/$arch
#Server = http://mirror.yer.az/archlinux/$repo/os/$arch
#Server = https://mirror.yer.az/archlinux/$repo/os/$arch
## Bangladesh
#Server = http://mirror.limda.net/archlinux/$repo/os/$arch
#Server = https://mirror.limda.net/archlinux/$repo/os/$arch
#Server = http://mirror.xeonbd.com/archlinux/$repo/os/$arch
#Server = https://mirror.xeonbd.com/archlinux/$repo/os/$arch
## Belarus
#Server = http://ftp.byfly.by/pub/archlinux/$repo/os/$arch
#Server = http://mirror.datacenter.by/pub/archlinux/$repo/os/$arch
## Belgium
#Server = http://mirror.1ago.be/archlinux/$repo/os/$arch
#Server = https://mirror.1ago.be/archlinux/$repo/os/$arch
#Server = http://mirror.jonas-prz.be/$repo/os/$arch
#Server = https://mirror.jonas-prz.be/$repo/os/$arch
#Server = https://archlinux.mirror-services.net/archlinux/$repo/os/$arch
#Server = http://mirror.tiguinet.net/arch/$repo/os/$arch
#Server = https://mirror.tiguinet.net/arch/$repo/os/$arch
## Brazil
#Server = http://archlinux.c3sl.ufpr.br/$repo/os/$arch
#Server = https://archlinux.c3sl.ufpr.br/$repo/os/$arch
#Server = http://br.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://br.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://mirror.ufscar.br/archlinux/$repo/os/$arch
#Server = https://mirror.ufscar.br/archlinux/$repo/os/$arch
#Server = http://mirrors.ic.unicamp.br/archlinux/$repo/os/$arch
#Server = https://mirrors.ic.unicamp.br/archlinux/$repo/os/$arch
## Bulgaria
#Server = http://mirror.host.ag/archlinux/$repo/os/$arch
#Server = http://mirror.telepoint.bg/archlinux/$repo/os/$arch
#Server = https://mirror.telepoint.bg/archlinux/$repo/os/$arch
#Server = http://mirrors.uni-plovdiv.net/archlinux/$repo/os/$arch
#Server = https://mirrors.uni-plovdiv.net/archlinux/$repo/os/$arch
## Canada
#Server = http://mirror.0xem.ma/arch/$repo/os/$arch
#Server = https://mirror.0xem.ma/arch/$repo/os/$arch
#Server = https://mirror.acadielinux.ca/mirror/arch/$repo/os/$arch
#Server = https://arch.mirror.winslow.cloud/$repo/os/$arch
#Server = http://ca.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://ca.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://mirror.cpsc.ucalgary.ca/mirror/archlinux.org/$repo/os/$arch
#Server = https://mirror.cpsc.ucalgary.ca/mirror/archlinux.org/$repo/os/$arch
#Server = http://mirror.csclub.uwaterloo.ca/archlinux/$repo/os/$arch
#Server = https://mirror.csclub.uwaterloo.ca/archlinux/$repo/os/$arch
#Server = http://mirror2.evolution-host.com/archlinux/$repo/os/$arch
#Server = https://mirror2.evolution-host.com/archlinux/$repo/os/$arch
#Server = https://stygian.failzero.net/mirror/archlinux/$repo/os/$arch
#Server = https://mirror.franscorack.com/archlinux/$repo/os/$arch
#Server = http://mirror.its.dal.ca/archlinux/$repo/os/$arch
#Server = http://mirror.quantum5.ca/archlinux/$repo/os/$arch
#Server = https://mirror.quantum5.ca/archlinux/$repo/os/$arch
#Server = http://muug.ca/mirror/archlinux/$repo/os/$arch
#Server = https://muug.ca/mirror/archlinux/$repo/os/$arch
#Server = http://mirrors.pablonara.com/archlinux/$repo/os/$arch
#Server = https://mirrors.pablonara.com/archlinux/$repo/os/$arch
#Server = https://upstream-1.pablonara.com/archlinux/$repo/os/$arch
#Server = http://archlinux.mirror.rafal.ca/$repo/os/$arch
#Server = http://mirror.scd31.com/arch/$repo/os/$arch
#Server = https://mirror.scd31.com/arch/$repo/os/$arch
#Server = http://mirror.xenyth.net/archlinux/$repo/os/$arch
#Server = https://mirror.xenyth.net/archlinux/$repo/os/$arch
## Chile
#Server = http://mirror.anquan.cl/archlinux/$repo/os/$arch
#Server = https://mirror.anquan.cl/archlinux/$repo/os/$arch
#Server = http://elmirror.cl/archlinux/$repo/os/$arch
#Server = https://elmirror.cl/archlinux/$repo/os/$arch
#Server = http://mirror.hnd.cl/archlinux/$repo/os/$arch
#Server = https://mirror.hnd.cl/archlinux/$repo/os/$arch
## China
#Server = http://mirrors.163.com/archlinux/$repo/os/$arch
#Server = http://mirrors.aliyun.com/archlinux/$repo/os/$arch
#Server = https://mirrors.aliyun.com/archlinux/$repo/os/$arch
#Server = http://mirrors.bfsu.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.bfsu.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirrors.cqu.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.cqu.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirrors.hit.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.hit.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirrors.hust.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.hust.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirrors.jlu.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.jlu.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirrors.jxust.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.jxust.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirror.lzu.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirrors.neusoft.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.neusoft.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirrors.nju.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.nju.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirror.nyist.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirror.nyist.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.sjtug.sjtu.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirrors.wsyu.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.wsyu.edu.cn/archlinux/$repo/os/$arch
#Server = https://mirrors.xjtu.edu.cn/archlinux/$repo/os/$arch
#Server = http://mirrors.zju.edu.cn/archlinux/$repo/os/$arch
## Colombia
#Server = http://mirrors.atlas.net.co/archlinux/$repo/os/$arch
#Server = https://mirrors.atlas.net.co/archlinux/$repo/os/$arch
#Server = http://edgeuno-bog2.mm.fcix.net/archlinux/$repo/os/$arch
#Server = https://edgeuno-bog2.mm.fcix.net/archlinux/$repo/os/$arch
#Server = http://mirrors.udenar.edu.co/archlinux/$repo/os/$arch
## Croatia
#Server = http://archlinux.iskon.hr/$repo/os/$arch
## Czechia
#Server = http://mirror.dkm.cz/archlinux/$repo/os/$arch
#Server = https://mirror.dkm.cz/archlinux/$repo/os/$arch
#Server = http://ftp.fi.muni.cz/pub/linux/arch/$repo/os/$arch
#Server = http://ftp.linux.cz/pub/linux/arch/$repo/os/$arch
#Server = https://europe.mirror.pkgbuild.com/$repo/os/$arch
#Server = http://gluttony.sin.cvut.cz/arch/$repo/os/$arch
#Server = https://gluttony.sin.cvut.cz/arch/$repo/os/$arch
#Server = https://archlinux.nic.cz/archlinux/$repo/os/$arch
#Server = http://ftp.sh.cvut.cz/arch/$repo/os/$arch
#Server = https://ftp.sh.cvut.cz/arch/$repo/os/$arch
#Server = http://mirror.vpsfree.cz/archlinux/$repo/os/$arch
## Denmark
#Server = http://mirrors.dotsrc.org/archlinux/$repo/os/$arch
#Server = https://mirrors.dotsrc.org/archlinux/$repo/os/$arch
#Server = http://mirror.group.one/archlinux/$repo/os/$arch
#Server = https://mirror.group.one/archlinux/$repo/os/$arch
#Server = https://mirror.it-privat.dk/arch/$repo/os/$arch
## Ecuador
#Server = http://mirror.cedia.org.ec/archlinux/$repo/os/$arch
#Server = https://mirror.linux.ec/archlinux/$repo/os/$arch
## Estonia
#Server = http://mirror.cspacehostings.com/archlinux/$repo/os/$arch
#Server = https://mirror.cspacehostings.com/archlinux/$repo/os/$arch
#Server = http://repo.br.ee/arch/$repo/os/$arch
#Server = https://repo.br.ee/arch/$repo/os/$arch
#Server = http://mirrors.xtom.ee/archlinux/$repo/os/$arch
#Server = https://mirrors.xtom.ee/archlinux/$repo/os/$arch
## Finland
#Server = https://archlinux.doridian.net/$repo/os/$arch
#Server = http://cdnmirror.com/archlinux/$repo/os/$arch
#Server = https://cdnmirror.com/archlinux/$repo/os/$arch
#Server = http://arch.mirror.far.fi/$repo/os/$arch
#Server = http://mirror.5i.fi/archlinux/$repo/os/$arch
#Server = https://mirror.5i.fi/archlinux/$repo/os/$arch
#Server = https://fi.arch.niranjan.co/$repo/os/$arch
#Server = https://mirror1.sl-chat.ru/archlinux/$repo/os/$arch
#Server = https://mirror.srv.fail/archlinux/$repo/os/$arch
#Server = http://mirror.wuki.li/archlinux/$repo/os/$arch
#Server = https://mirror.wuki.li/archlinux/$repo/os/$arch
#Server = http://arch.yhtez.xyz/$repo/os/$arch
#Server = https://arch.yhtez.xyz/$repo/os/$arch
## France
#Server = http://mirror.archlinux.ikoula.com/archlinux/$repo/os/$arch
#Server = https://elda.asgardius.company/archlinux/$repo/os/$arch
#Server = http://mirror.bakertelekom.fr/Arch/$repo/os/$arch
#Server = https://mirror.bakertelekom.fr/Arch/$repo/os/$arch
#Server = http://fr.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://fr.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://mirror.cyberbits.eu/archlinux/$repo/os/$arch
#Server = https://mirror.cyberbits.eu/archlinux/$repo/os/$arch
#Server = http://archlinux.datagr.am/$repo/os/$arch
#Server = https://mirrors.eric.ovh/arch/$repo/os/$arch
#Server = http://mirrors.gandi.net/archlinux/$repo/os/$arch
#Server = https://mirrors.gandi.net/archlinux/$repo/os/$arch
#Server = http://archmirror.hogwarts.fr/$repo/os/$arch
#Server = https://archmirror.hogwarts.fr/$repo/os/$arch
#Server = http://mirror.its-tps.fr/archlinux/$repo/os/$arch
#Server = https://mirror.its-tps.fr/archlinux/$repo/os/$arch
#Server = https://mirrors.jtremesay.org/archlinux/$repo/os/$arch
#Server = http://mirror.lastmikoi.net/archlinux/$repo/os/$arch
#Server = http://archlinux.mailtunnel.eu/$repo/os/$arch
#Server = https://archlinux.mailtunnel.eu/$repo/os/$arch
#Server = http://f.matthieul.dev/mirror/archlinux/$repo/os/$arch
#Server = https://f.matthieul.dev/mirror/archlinux/$repo/os/$arch
#Server = http://mir.archlinux.fr/$repo/os/$arch
#Server = http://mirror.oldsql.cc/archlinux/$repo/os/$arch
#Server = https://mirror.oldsql.cc/archlinux/$repo/os/$arch
#Server = http://archlinux.mirrors.ovh.net/archlinux/$repo/os/$arch
#Server = https://archlinux.mirrors.ovh.net/archlinux/$repo/os/$arch
#Server = http://mirror.peeres-telecom.fr/archlinux/$repo/os/$arch
#Server = https://mirror.peeres-telecom.fr/archlinux/$repo/os/$arch
#Server = http://mirror.rznet.fr/archlinux/$repo/os/$arch
#Server = https://mirror.rznet.fr/archlinux/$repo/os/$arch
#Server = https://mirror.smayzy.ovh/archlinux/$repo/os/$arch
#Server = http://arch.syxpi.fr/arch/$repo/os/$arch
#Server = https://arch.syxpi.fr/arch/$repo/os/$arch
#Server = https://mirror.thekinrar.fr/archlinux/$repo/os/$arch
#Server = http://mirror.theo546.fr/archlinux/$repo/os/$arch
#Server = https://mirror.theo546.fr/archlinux/$repo/os/$arch
#Server = http://mirror.trap.moe/archlinux/$repo/os/$arch
#Server = https://mirror.trap.moe/archlinux/$repo/os/$arch
#Server = http://ftp.u-strasbg.fr/linux/distributions/archlinux/$repo/os/$arch
#Server = https://mirror.wormhole.eu/archlinux/$repo/os/$arch
#Server = http://arch.yourlabs.org/$repo/os/$arch
#Server = https://arch.yourlabs.org/$repo/os/$arch
## Georgia
#Server = http://archlinux.grena.ge/$repo/os/$arch
#Server = https://archlinux.grena.ge/$repo/os/$arch
## Germany
#Server = http://mirror.23m.com/archlinux/$repo/os/$arch
#Server = https://mirror.23m.com/archlinux/$repo/os/$arch
#Server = http://ftp.agdsn.de/pub/mirrors/archlinux/$repo/os/$arch
#Server = https://ftp.agdsn.de/pub/mirrors/archlinux/$repo/os/$arch
#Server = http://mirrors.aminvakil.com/archlinux/$repo/os/$arch
#Server = https://mirrors.aminvakil.com/archlinux/$repo/os/$arch
#Server = http://artfiles.org/archlinux.org/$repo/os/$arch
#Server = https://mirror.bethselamin.de/$repo/os/$arch
#Server = https://de.repo.c48.uk/arch/$repo/os/$arch
#Server = http://de.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://de.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://mirror.clientvps.com/archlinux/$repo/os/$arch
#Server = https://mirror.clientvps.com/archlinux/$repo/os/$arch
#Server = http://mirror.cmt.de/archlinux/$repo/os/$arch
#Server = https://mirror.cmt.de/archlinux/$repo/os/$arch
#Server = http://os.codefionn.eu/archlinux/$repo/os/$arch
#Server = https://os.codefionn.eu/archlinux/$repo/os/$arch
#Server = http://mirror-de-1.cutie.dating/archlinux/$repo/os/$arch
#Server = https://mirror-de-1.cutie.dating/archlinux/$repo/os/$arch
#Server = https://mirror.dogado.de/archlinux/$repo/os/$arch
#Server = http://ftp.fau.de/archlinux/$repo/os/$arch
#Server = https://ftp.fau.de/archlinux/$repo/os/$arch
#Server = https://pkg.fef.moe/archlinux/$repo/os/$arch
#Server = https://dist-mirror.fem.tu-ilmenau.de/archlinux/$repo/os/$arch
#Server = https://berlin.mirror.pkgbuild.com/$repo/os/$arch
#Server = http://ftp.gwdg.de/pub/linux/archlinux/$repo/os/$arch
#Server = https://files.hadiko.de/pub/dists/arch/$repo/os/$arch
#Server = http://ftp.hosteurope.de/mirror/ftp.archlinux.org/$repo/os/$arch
#Server = http://ftp-stud.hs-esslingen.de/pub/Mirrors/archlinux/$repo/os/$arch
#Server = http://mirror.hugo-betrugo.de/archlinux/$repo/os/$arch
#Server = https://mirror.hugo-betrugo.de/archlinux/$repo/os/$arch
#Server = http://mirror.informatik.tu-freiberg.de/arch/$repo/os/$arch
#Server = https://mirror.informatik.tu-freiberg.de/arch/$repo/os/$arch
#Server = http://mirror.as20647.net/archlinux/$repo/os/$arch
#Server = http://mirror.ipb.de/archlinux/$repo/os/$arch
#Server = https://mirror.as20647.net/archlinux/$repo/os/$arch
#Server = https://mirror.ipb.de/archlinux/$repo/os/$arch
#Server = http://archlinux.mirror.iphh.net/$repo/os/$arch
#Server = http://mirrors.janbruckner.de/archlinux/$repo/os/$arch
#Server = https://mirrors.janbruckner.de/archlinux/$repo/os/$arch
#Server = http://arch.jensgutermuth.de/$repo/os/$arch
#Server = https://arch.jensgutermuth.de/$repo/os/$arch
#Server = https://de.arch.mirror.kescher.at/$repo/os/$arch
#Server = http://mirror.kumi.systems/archlinux/$repo/os/$arch
#Server = https://mirror.kumi.systems/archlinux/$repo/os/$arch
#Server = http://mirror.fra10.de.leaseweb.net/archlinux/$repo/os/$arch
#Server = https://mirror.fra10.de.leaseweb.net/archlinux/$repo/os/$arch
#Server = http://mirror.metalgamer.eu/archlinux/$repo/os/$arch
#Server = https://mirror.metalgamer.eu/archlinux/$repo/os/$arch
#Server = http://mirror.lcarilla.de/archlinux/$repo/os/$arch
#Server = https://mirror.lcarilla.de/archlinux/$repo/os/$arch
#Server = http://mirror.moson.org/arch/$repo/os/$arch
#Server = https://mirror.moson.org/arch/$repo/os/$arch
#Server = http://mirrors.n-ix.net/archlinux/$repo/os/$arch
#Server = https://mirrors.n-ix.net/archlinux/$repo/os/$arch
#Server = http://mirror.netcologne.de/archlinux/$repo/os/$arch
#Server = https://mirror.netcologne.de/archlinux/$repo/os/$arch
#Server = https://de.arch.niranjan.co/$repo/os/$arch
#Server = http://mirrors.niyawe.de/archlinux/$repo/os/$arch
#Server = https://mirrors.niyawe.de/archlinux/$repo/os/$arch
#Server = http://packages.oth-regensburg.de/archlinux/$repo/os/$arch
#Server = https://packages.oth-regensburg.de/archlinux/$repo/os/$arch
#Server = http://arch.owochle.app/$repo/os/$arch
#Server = https://arch.owochle.app/$repo/os/$arch
#Server = http://mirror.pagenotfound.de/archlinux/$repo/os/$arch
#Server = https://mirror.pagenotfound.de/archlinux/$repo/os/$arch
#Server = http://arch.phinau.de/$repo/os/$arch
#Server = https://arch.phinau.de/$repo/os/$arch
#Server = https://mirror.pseudoform.org/$repo/os/$arch
#Server = http://mirrors.purring.online/arch/$repo/os/$arch
#Server = https://mirrors.purring.online/arch/$repo/os/$arch
#Server = https://archlinux.richard-neumann.de/$repo/os/$arch
#Server = http://ftp.halifax.rwth-aachen.de/archlinux/$repo/os/$arch
#Server = https://ftp.halifax.rwth-aachen.de/archlinux/$repo/os/$arch
#Server = http://linux.rz.rub.de/archlinux/$repo/os/$arch
#Server = http://mirror.selfnet.de/archlinux/$repo/os/$arch
#Server = https://mirror.selfnet.de/archlinux/$repo/os/$arch
#Server = https://de-nue.soulharsh007.dev/archlinux/$repo/os/$arch
#Server = http://ftp.spline.inf.fu-berlin.de/mirrors/archlinux/$repo/os/$arch
#Server = https://ftp.spline.inf.fu-berlin.de/mirrors/archlinux/$repo/os/$arch
#Server = http://mirror.sunred.org/archlinux/$repo/os/$arch
#Server = https://mirror.sunred.org/archlinux/$repo/os/$arch
#Server = http://archlinux.thaller.ws/$repo/os/$arch
#Server = https://archlinux.thaller.ws/$repo/os/$arch
#Server = https://mirror.thereisno.page/archlinux/$repo/os/$arch
#Server = http://ftp.tu-chemnitz.de/pub/linux/archlinux/$repo/os/$arch
#Server = http://mirror.ubrco.de/archlinux/$repo/os/$arch
#Server = https://mirror.ubrco.de/archlinux/$repo/os/$arch
#Server = http://ftp.uni-bayreuth.de/linux/archlinux/$repo/os/$arch
#Server = http://ftp.uni-hannover.de/archlinux/$repo/os/$arch
#Server = http://ftp.uni-kl.de/pub/linux/archlinux/$repo/os/$arch
#Server = https://arch.unixpeople.org/$repo/os/$arch
#Server = http://mirrors.xtom.de/archlinux/$repo/os/$arch
#Server = https://mirrors.xtom.de/archlinux/$repo/os/$arch
## Greece
#Server = http://ftp.cc.uoc.gr/mirrors/linux/archlinux/$repo/os/$arch
#Server = https://repo.greeklug.gr/data/pub/linux/archlinux/$repo/os/$arch
#Server = http://ftp.otenet.gr/linux/archlinux/$repo/os/$arch
## Hong Kong
#Server = https://hk.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://mirror-hk.koddos.net/archlinux/$repo/os/$arch
#Server = http://hkg.mirror.rackspace.com/archlinux/$repo/os/$arch
#Server = https://hkg.mirror.rackspace.com/archlinux/$repo/os/$arch
#Server = https://arch-mirror.wtako.net/$repo/os/$arch
#Server = http://mirror.xtom.com.hk/archlinux/$repo/os/$arch
#Server = https://mirror.xtom.com.hk/archlinux/$repo/os/$arch
## Hungary
#Server = https://ftp.ek-cer.hu/pub/mirrors/ftp.archlinux.org/$repo/os/$arch
#Server = http://nova.quantum-mirror.hu/mirrors/pub/archlinux/$repo/os/$arch
#Server = http://quantum-mirror.hu/mirrors/pub/archlinux/$repo/os/$arch
#Server = http://super.quantum-mirror.hu/mirrors/pub/archlinux/$repo/os/$arch
#Server = https://nova.quantum-mirror.hu/mirrors/pub/archlinux/$repo/os/$arch
#Server = https://quantum-mirror.hu/mirrors/pub/archlinux/$repo/os/$arch
#Server = https://super.quantum-mirror.hu/mirrors/pub/archlinux/$repo/os/$arch
## Iceland
#Server = http://is.mirror.flokinet.net/archlinux/$repo/os/$arch
#Server = https://is.mirror.flokinet.net/archlinux/$repo/os/$arch
## India
#Server = https://mirrors.abhy.me/archlinux/$repo/os/$arch
#Server = https://mirror.del2.albony.in/archlinux/$repo/os/$arch
#Server = https://mirror.maa.albony.in/archlinux/$repo/os/$arch
#Server = http://in.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://in.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://in-mirror.garudalinux.org/archlinux/$repo/os/$arch
#Server = https://in-mirror.garudalinux.org/archlinux/$repo/os/$arch
#Server = https://archlinux.kushwanthreddy.com/$repo/os/$arch
#Server = https://in.arch.niranjan.co/$repo/os/$arch
#Server = http://mirrors.nxtgen.com/archlinux-mirror/$repo/os/$arch
#Server = https://mirrors.nxtgen.com/archlinux-mirror/$repo/os/$arch
#Server = http://mirror.sahil.world/archlinux/$repo/os/$arch
#Server = https://mirror.sahil.world/archlinux/$repo/os/$arch
#Server = http://mirrors.saswata.cc/archlinux/$repo/os/$arch
#Server = https://mirrors.saswata.cc/archlinux/$repo/os/$arch
## Indonesia
#Server = http://mirror.citrahost.com/archlinux/$repo/os/$arch
#Server = https://mirror.citrahost.com/archlinux/$repo/os/$arch
#Server = http://mirror.gi.co.id/archlinux/$repo/os/$arch
#Server = https://mirror.gi.co.id/archlinux/$repo/os/$arch
#Server = http://kebo.pens.ac.id/archlinux/$repo/os/$arch
#Server = http://mirror.ditatompel.com/archlinux/$repo/os/$arch
#Server = https://mirror.ditatompel.com/archlinux/$repo/os/$arch
#Server = http://mirror.papua.go.id/archlinux/$repo/os/$arch
#Server = https://mirror.papua.go.id/archlinux/$repo/os/$arch
#Server = https://kacabenggala.uny.ac.id/archlinux/$repo/os/$arch
## Iran
#Server = http://mirror.arvancloud.ir/archlinux/$repo/os/$arch
#Server = https://mirror.arvancloud.ir/archlinux/$repo/os/$arch
#Server = http://repo.iut.ac.ir/repo/archlinux/$repo/os/$arch
#Server = http://mirror.mobinhost.com/archlinux/$repo/os/$arch
#Server = https://mirror.mobinhost.com/archlinux/$repo/os/$arch
## Israel
#Server = http://archlinux.interhost.co.il/$repo/os/$arch
#Server = https://archlinux.interhost.co.il/$repo/os/$arch
#Server = http://mirror.isoc.org.il/pub/archlinux/$repo/os/$arch
#Server = https://mirror.isoc.org.il/pub/archlinux/$repo/os/$arch
## Italy
#Server = http://it.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://it.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://archlinux.mirror.garr.it/archlinux/$repo/os/$arch
#Server = https://arch.mirror.hyperbit.it/$repo/os/$arch
#Server = http://archlinux.mirror.server24.net/$repo/os/$arch
#Server = https://archlinux.mirror.server24.net/$repo/os/$arch
## Japan
#Server = http://mirrors.cat.net/archlinux/$repo/os/$arch
#Server = https://mirrors.cat.net/archlinux/$repo/os/$arch
#Server = http://jp.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://jp.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://ftp.tsukuba.wide.ad.jp/Linux/archlinux/$repo/os/$arch
#Server = http://ftp.jaist.ac.jp/pub/Linux/ArchLinux/$repo/os/$arch
#Server = https://ftp.jaist.ac.jp/pub/Linux/ArchLinux/$repo/os/$arch
#Server = http://mirror.rain.ne.jp/archlinux/$repo/os/$arch
#Server = https://mirror.rain.ne.jp/archlinux/$repo/os/$arch
## Kazakhstan
#Server = http://mirror.ps.kz/archlinux/$repo/os/$arch
#Server = https://mirror.ps.kz/archlinux/$repo/os/$arch
## Kenya
#Server = http://archlinux.mirror.liquidtelecom.com/$repo/os/$arch
#Server = https://archlinux.mirror.liquidtelecom.com/$repo/os/$arch
## Latvia
#Server = http://ftp.linux.edu.lv/archlinux/$repo/os/$arch
#Server = https://ftp.linux.edu.lv/archlinux/$repo/os/$arch
#Server = http://archlinux.koyanet.lv/archlinux/$repo/os/$arch
#Server = https://archlinux.koyanet.lv/archlinux/$repo/os/$arch
## Lithuania
#Server = http://mirrors.atviras.lt/archlinux/$repo/os/$arch
#Server = https://mirrors.atviras.lt/archlinux/$repo/os/$arch
#Server = http://mirror.sinirlan.net/archlinux/$repo/os/$arch
#Server = https://mirror.sinirlan.net/archlinux/$repo/os/$arch
## Luxembourg
#Server = http://arch-lux.spirex.me/$repo/os/$arch
#Server = https://arch-lux.spirex.me/$repo/os/$arch
## Mauritius
#Server = http://archlinux-mirror.cloud.mu/$repo/os/$arch
#Server = https://archlinux-mirror.cloud.mu/$repo/os/$arch
## Mexico
#Server = http://lidsol.fi-b.unam.mx/archlinux/$repo/os/$arch
#Server = https://lidsol.fi-b.unam.mx/archlinux/$repo/os/$arch
#Server = https://arch.jsc.mx/$repo/os/$arch
## Moldova
#Server = http://mirror.hosthink.net/arch/$repo/os/$arch
#Server = https://mirror.hosthink.net/arch/$repo/os/$arch
#Server = http://mirror.ihost.md/archlinux/$repo/os/$arch
#Server = https://mirror.ihost.md/archlinux/$repo/os/$arch
#Server = http://mirror.mangohost.net/archlinux/$repo/os/$arch
#Server = https://mirror.mangohost.net/archlinux/$repo/os/$arch
## Morocco
#Server = http://mirror.abderraziq.com/archlinux/$repo/os/$arch
#Server = https://mirror.abderraziq.com/archlinux/$repo/os/$arch
## Netherlands
#Server = http://ams.nl.mirrors.bjg.at/arch/$repo/os/$arch
#Server = https://ams.nl.mirrors.bjg.at/arch/$repo/os/$arch
#Server = http://mirror.bouwhuis.network/archlinux/$repo/os/$arch
#Server = https://mirror.bouwhuis.network/archlinux/$repo/os/$arch
#Server = http://mirror.nl.cdn-perfprod.com/archlinux/$repo/os/$arch
#Server = https://mirror.nl.cdn-perfprod.com/archlinux/$repo/os/$arch
#Server = http://nl.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://nl.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://mirror.cj2.nl/archlinux/$repo/os/$arch
#Server = https://mirror.cj2.nl/archlinux/$repo/os/$arch
#Server = http://mirrors.evoluso.com/archlinux/$repo/os/$arch
#Server = http://nl.mirror.flokinet.net/archlinux/$repo/os/$arch
#Server = https://nl.mirror.flokinet.net/archlinux/$repo/os/$arch
#Server = https://mirror.iusearchbtw.nl/$repo/os/$arch
#Server = http://mirror.koddos.net/archlinux/$repo/os/$arch
#Server = http://arch.mirrors.lavatech.top/$repo/os/$arch
#Server = https://arch.mirrors.lavatech.top/$repo/os/$arch
#Server = http://mirror.ams1.nl.leaseweb.net/archlinux/$repo/os/$arch
#Server = https://mirror.ams1.nl.leaseweb.net/archlinux/$repo/os/$arch
#Server = http://archlinux.mirror.liteserver.nl/$repo/os/$arch
#Server = https://archlinux.mirror.liteserver.nl/$repo/os/$arch
#Server = http://mirror.lyrahosting.com/archlinux/$repo/os/$arch
#Server = https://mirror.lyrahosting.com/archlinux/$repo/os/$arch
#Server = http://mirror.mijn.host/archlinux/$repo/os/$arch
#Server = https://mirror.mijn.host/archlinux/$repo/os/$arch
#Server = http://ftp.nluug.nl/os/Linux/distr/archlinux/$repo/os/$arch
#Server = http://ftp.snt.utwente.nl/pub/os/linux/archlinux/$repo/os/$arch
#Server = http://archlinux.mirror.wearetriple.com/$repo/os/$arch
#Server = https://archlinux.mirror.wearetriple.com/$repo/os/$arch
#Server = http://mirrors.xtom.nl/archlinux/$repo/os/$arch
#Server = https://mirrors.xtom.nl/archlinux/$repo/os/$arch
## New Caledonia
#Server = http://mirror.lagoon.nc/pub/archlinux/$repo/os/$arch
#Server = http://archlinux.nautile.nc/archlinux/$repo/os/$arch
#Server = https://archlinux.nautile.nc/archlinux/$repo/os/$arch
## New Zealand
#Server = http://mirror.2degrees.nz/archlinux/$repo/os/$arch
#Server = https://mirror.2degrees.nz/archlinux/$repo/os/$arch
#Server = http://mirror.fsmg.org.nz/archlinux/$repo/os/$arch
#Server = https://mirror.fsmg.org.nz/archlinux/$repo/os/$arch
#Server = https://nz.arch.niranjan.co/$repo/os/$arch
#Server = https://archlinux.ourhome.kiwi/$repo/os/$arch
## North Macedonia
#Server = http://arch.softver.org.mk/archlinux/$repo/os/$arch
#Server = http://mirror.onevip.mk/archlinux/$repo/os/$arch
#Server = http://mirror.t-home.mk/archlinux/$repo/os/$arch
#Server = https://mirror.t-home.mk/archlinux/$repo/os/$arch
## Norway
#Server = http://mirror.archlinux.no/$repo/os/$arch
#Server = https://mirror.archlinux.no/$repo/os/$arch
#Server = http://archlinux.uib.no/$repo/os/$arch
#Server = http://mirror.neuf.no/archlinux/$repo/os/$arch
#Server = https://mirror.neuf.no/archlinux/$repo/os/$arch
## Poland
#Server = http://ftp.icm.edu.pl/pub/Linux/dist/archlinux/$repo/os/$arch
#Server = https://ftp.icm.edu.pl/pub/Linux/dist/archlinux/$repo/os/$arch
#Server = http://mirror.juniorjpdj.pl/archlinux/$repo/os/$arch
#Server = https://mirror.juniorjpdj.pl/archlinux/$repo/os/$arch
#Server = http://arch.midov.pl/arch/$repo/os/$arch
#Server = https://arch.midov.pl/arch/$repo/os/$arch
#Server = https://mirror.przekichane.pl/archlinux/$repo/os/$arch
#Server = http://ftp.psnc.pl/linux/archlinux/$repo/os/$arch
#Server = https://ftp.psnc.pl/linux/archlinux/$repo/os/$arch
#Server = http://arch.sakamoto.pl/$repo/os/$arch
#Server = https://arch.sakamoto.pl/$repo/os/$arch
## Portugal
#Server = http://mirror.barata.pt/archlinux/$repo/os/$arch
#Server = https://mirror.barata.pt/archlinux/$repo/os/$arch
#Server = http://glua.ua.pt/pub/archlinux/$repo/os/$arch
#Server = https://glua.ua.pt/pub/archlinux/$repo/os/$arch
#Server = http://mirrors.up.pt/pub/archlinux/$repo/os/$arch
#Server = https://mirrors.up.pt/pub/archlinux/$repo/os/$arch
#Server = http://ftp.rnl.tecnico.ulisboa.pt/pub/archlinux/$repo/os/$arch
#Server = https://ftp.rnl.tecnico.ulisboa.pt/pub/archlinux/$repo/os/$arch
## Romania
#Server = http://mirror.ro.cdn-perfprod.com/archlinux/$repo/os/$arch
#Server = https://mirror.ro.cdn-perfprod.com/archlinux/$repo/os/$arch
#Server = http://mirrors.chroot.ro/archlinux/$repo/os/$arch
#Server = https://mirrors.chroot.ro/archlinux/$repo/os/$arch
#Server = http://mirror.efect.ro/archlinux/$repo/os/$arch
#Server = https://mirror.efect.ro/archlinux/$repo/os/$arch
#Server = http://ro.mirror.flokinet.net/archlinux/$repo/os/$arch
#Server = https://ro.mirror.flokinet.net/archlinux/$repo/os/$arch
#Server = http://mirrors.hosterion.ro/archlinux/$repo/os/$arch
#Server = https://mirrors.hosterion.ro/archlinux/$repo/os/$arch
#Server = http://mirrors.hostico.ro/archlinux/$repo/os/$arch
#Server = https://mirrors.hostico.ro/archlinux/$repo/os/$arch
#Server = http://archlinux.mirrors.linux.ro/$repo/os/$arch
#Server = http://mirrors.nav.ro/archlinux/$repo/os/$arch
#Server = https://ro.arch.niranjan.co/$repo/os/$arch
#Server = http://mirrors.nxthost.com/archlinux/$repo/os/$arch
#Server = https://mirrors.nxthost.com/archlinux/$repo/os/$arch
#Server = http://mirrors.pidginhost.com/arch/$repo/os/$arch
#Server = https://mirrors.pidginhost.com/arch/$repo/os/$arch
## Russia
#Server = http://archlinux.gay/archlinux/$repo/os/$arch
#Server = https://archlinux.gay/archlinux/$repo/os/$arch
#Server = http://ru.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://ru.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://mirror.kpfu.ru/archlinux/$repo/os/$arch
#Server = https://mirror.kpfu.ru/archlinux/$repo/os/$arch
#Server = http://mirror.nw-sys.ru/archlinux/$repo/os/$arch
#Server = https://mirror.nw-sys.ru/archlinux/$repo/os/$arch
#Server = http://mirrors.powernet.com.ru/archlinux/$repo/os/$arch
#Server = http://repository.su/archlinux/$repo/os/$arch
#Server = https://repository.su/archlinux/$repo/os/$arch
#Server = http://web.sketserv.ru/archlinux/$repo/os/$arch
#Server = https://web.sketserv.ru/archlinux/$repo/os/$arch
#Server = https://mirror2.sl-chat.ru/archlinux/$repo/os/$arch
#Server = https://mirror3.sl-chat.ru/archlinux/$repo/os/$arch
#Server = http://mirror.truenetwork.ru/archlinux/$repo/os/$arch
#Server = https://mirror.truenetwork.ru/archlinux/$repo/os/$arch
#Server = http://vladivostokst.ru/archlinux/$repo/os/$arch
#Server = https://vladivostokst.ru/archlinux/$repo/os/$arch
#Server = http://mirror.yandex.ru/archlinux/$repo/os/$arch
#Server = https://mirror.yandex.ru/archlinux/$repo/os/$arch
## Réunion
#Server = http://arch.mithril.re/$repo/os/$arch
## Saudi Arabia
#Server = http://sa.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://sa.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://mirror.maeen.sa/arch-mirror/$repo/os/$arch
#Server = https://mirror.maeen.sa/arch-mirror/$repo/os/$arch
## Serbia
#Server = http://mirror.pmf.kg.ac.rs/archlinux/$repo/os/$arch
#Server = http://mirror1.sox.rs/archlinux/$repo/os/$arch
#Server = https://mirror1.sox.rs/archlinux/$repo/os/$arch
## Singapore
#Server = http://mirror.aktkn.sg/archlinux/$repo/os/$arch
#Server = https://mirror.aktkn.sg/archlinux/$repo/os/$arch
#Server = http://mirror.sg.cdn-perfprod.com/archlinux/$repo/os/$arch
#Server = https://mirror.sg.cdn-perfprod.com/archlinux/$repo/os/$arch
#Server = http://sg.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://sg.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://download.nus.edu.sg/mirror/archlinux/$repo/os/$arch
#Server = https://singapore.mirror.pkgbuild.com/$repo/os/$arch
#Server = http://mirror.guillaumea.fr/archlinux/$repo/os/$arch
#Server = https://mirror.guillaumea.fr/archlinux/$repo/os/$arch
#Server = http://mirror.jingk.ai/archlinux/$repo/os/$arch
#Server = https://mirror.jingk.ai/archlinux/$repo/os/$arch
#Server = https://sg.arch.niranjan.co/$repo/os/$arch
#Server = http://ossmirror.mycloud.services/os/linux/archlinux/$repo/os/$arch
#Server = http://mirror.sg.gs/archlinux/$repo/os/$arch
#Server = https://mirror.sg.gs/archlinux/$repo/os/$arch
## Slovakia
#Server = http://ftp.energotel.sk/pub/linux/arch/$repo/os/$arch
#Server = https://ftp.energotel.sk/pub/linux/arch/$repo/os/$arch
#Server = http://mirror.lnx.sk/pub/linux/archlinux/$repo/os/$arch
#Server = https://mirror.lnx.sk/pub/linux/archlinux/$repo/os/$arch
#Server = http://tux.rainside.sk/archlinux/$repo/os/$arch
## Slovenia
#Server = https://mirror.archlinux.si/$repo/os/$arch
#Server = https://www.sooftware.com/mirrors/Arch-Linux/$repo/os/$arch
#Server = http://mirror.tux.si/arch/$repo/os/$arch
#Server = https://mirror.tux.si/arch/$repo/os/$arch
## South Africa
#Server = http://archlinux.za.mirror.allworldit.com/archlinux/$repo/os/$arch
#Server = https://archlinux.za.mirror.allworldit.com/archlinux/$repo/os/$arch
#Server = http://za.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://za.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://johannesburg.mirror.pkgbuild.com/$repo/os/$arch
#Server = http://mirror.is.co.za/mirror/archlinux.org/$repo/os/$arch
#Server = http://mirrors.urbanwave.co.za/archlinux/$repo/os/$arch
#Server = https://mirrors.urbanwave.co.za/archlinux/$repo/os/$arch
## South Korea
#Server = http://kr.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://kr.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://mirror.techlabs.co.kr/archlinux/$repo/os/$arch
#Server = https://mirror.techlabs.co.kr/archlinux/$repo/os/$arch
#Server = http://mirror.distly.kr/archlinux/$repo/os/$arch
#Server = https://mirror.distly.kr/archlinux/$repo/os/$arch
#Server = http://ftp.kaist.ac.kr/ArchLinux/$repo/os/$arch
#Server = http://ftp.hrts.kr/archlinux/$repo/os/$arch
#Server = https://ftp.hrts.kr/archlinux/$repo/os/$arch
#Server = http://mirror.keiminem.com/archlinux/$repo/os/$arch
#Server = http://mirror2.keiminem.com/archlinux/$repo/os/$arch
#Server = https://mirror.keiminem.com/archlinux/$repo/os/$arch
#Server = https://mirror2.keiminem.com/archlinux/$repo/os/$arch
#Server = https://mirror.krfoss.org/archlinux/$repo/os/$arch
#Server = http://ftp.lanet.kr/pub/archlinux/$repo/os/$arch
#Server = https://ftp.lanet.kr/pub/archlinux/$repo/os/$arch
#Server = http://mirror.siwoo.org/archlinux/$repo/os/$arch
#Server = https://mirror.siwoo.org/archlinux/$repo/os/$arch
## Spain
#Server = http://mirror.es.cdn-perfprod.com/archlinux/$repo/os/$arch
#Server = https://mirror.es.cdn-perfprod.com/archlinux/$repo/os/$arch
#Server = http://es.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://es.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://nox.panibrez.com/archlinux/$repo/os/$arch
#Server = http://mirror.raiolanetworks.com/archlinux/$repo/os/$arch
#Server = https://mirror.raiolanetworks.com/archlinux/$repo/os/$arch
#Server = https://ftp.rediris.es/mirror/archlinux/$repo/os/$arch
## Sweden
#Server = http://mirror.accum.se/mirror/archlinux/$repo/os/$arch
#Server = https://mirror.accum.se/mirror/archlinux/$repo/os/$arch
#Server = https://mirror.braindrainlan.nu/archlinux/$repo/os/$arch
#Server = http://ftpmirror.infania.net/mirror/archlinux/$repo/os/$arch
#Server = http://ftp.ludd.ltu.se/mirrors/archlinux/$repo/os/$arch
#Server = https://ftp.ludd.ltu.se/mirrors/archlinux/$repo/os/$arch
#Server = http://ftp.lysator.liu.se/pub/archlinux/$repo/os/$arch
#Server = https://ftp.lysator.liu.se/pub/archlinux/$repo/os/$arch
#Server = http://mirror.bahnhof.net/pub/archlinux/$repo/os/$arch
#Server = https://mirror.bahnhof.net/pub/archlinux/$repo/os/$arch
#Server = http://ftp.myrveln.se/pub/linux/archlinux/$repo/os/$arch
#Server = https://ftp.myrveln.se/pub/linux/archlinux/$repo/os/$arch
#Server = https://mirror.osbeck.com/archlinux/$repo/os/$arch
#Server = http://mirror.retropc.se/archlinux/$repo/os/$arch
#Server = https://mirror.retropc.se/archlinux/$repo/os/$arch
#Server = http://mirror.tedwall.se/archlinux/$repo/os/$arch
#Server = https://mirror.tedwall.se/archlinux/$repo/os/$arch
## Switzerland
#Server = http://pkg.adfinis-on-exoscale.ch/archlinux-pkgbuild/$repo/os/$arch
#Server = http://pkg.adfinis-on-exoscale.ch/archlinux/$repo/os/$arch
#Server = https://pkg.adfinis-on-exoscale.ch/archlinux-pkgbuild/$repo/os/$arch
#Server = https://pkg.adfinis-on-exoscale.ch/archlinux/$repo/os/$arch
#Server = http://mirror.arch-linux.ch/archlinux/$repo/os/$arch
#Server = https://mirror.arch-linux.ch/archlinux/$repo/os/$arch
#Server = https://archlinux.lan.brgn.ch/archlinux/$repo/os/$arch
#Server = http://ch.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://ch.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://mirror.hb9hil.org/archlinux/$repo/os/$arch
#Server = https://mirror.hb9hil.org/archlinux/$repo/os/$arch
#Server = http://mirror.init7.net/archlinux/$repo/os/$arch
#Server = https://mirror.init7.net/archlinux/$repo/os/$arch
#Server = http://mirror.metanet.ch/archlinux/$repo/os/$arch
#Server = https://mirror.metanet.ch/archlinux/$repo/os/$arch
#Server = http://mirror.puzzle.ch/archlinux/$repo/os/$arch
#Server = https://mirror.puzzle.ch/archlinux/$repo/os/$arch
#Server = https://theswissbay.ch/archlinux/$repo/os/$arch
#Server = https://mirror.ungleich.ch/mirror/packages/archlinux/$repo/os/$arch
#Server = https://mirror.worldhotspot.org/archlinux/$repo/os/$arch
## Taiwan
#Server = http://mirror.archlinux.tw/ArchLinux/$repo/os/$arch
#Server = https://mirror.archlinux.tw/ArchLinux/$repo/os/$arch
#Server = http://archlinux.ccns.ncku.edu.tw/archlinux/$repo/os/$arch
#Server = https://archlinux.ccns.ncku.edu.tw/archlinux/$repo/os/$arch
#Server = http://tw.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://tw.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://free.nchc.org.tw/arch/$repo/os/$arch
#Server = https://taipei.mirror.pkgbuild.com/$repo/os/$arch
#Server = http://archlinux.cs.nycu.edu.tw/$repo/os/$arch
#Server = https://archlinux.cs.nycu.edu.tw/$repo/os/$arch
#Server = http://mirror.twds.com.tw/archlinux/$repo/os/$arch
#Server = https://mirror.twds.com.tw/archlinux/$repo/os/$arch
## Thailand
#Server = https://mirror.cyberbits.asia/archlinux/$repo/os/$arch
#Server = http://mirror.kku.ac.th/archlinux/$repo/os/$arch
#Server = https://mirror.kku.ac.th/archlinux/$repo/os/$arch
## Türkiye
#Server = http://ftp.linux.org.tr/archlinux/$repo/os/$arch
#Server = https://tr.arch.niranjan.co/$repo/os/$arch
#Server = http://mirror.nucc.tr/arch/$repo/os/$arch
#Server = https://mirror.nucc.tr/arch/$repo/os/$arch
#Server = http://mirror.timtal.com.tr/archlinux/$repo/os/$arch
#Server = https://mirror.timtal.com.tr/archlinux/$repo/os/$arch
## Ukraine
#Server = http://distrohub.kyiv.ua/archlinux/$repo/os/$arch
#Server = https://distrohub.kyiv.ua/archlinux/$repo/os/$arch
#Server = http://repo.hyron.dev/archlinux/$repo/os/$arch
#Server = https://repo.hyron.dev/archlinux/$repo/os/$arch
#Server = http://mirror.hntr.pp.ua/archlinux/$repo/os/$arch
#Server = https://mirror.hntr.pp.ua/archlinux/$repo/os/$arch
#Server = http://mirror.hostiko.network/archlinux/$repo/os/$arch
#Server = https://mirror.hostiko.network/archlinux/$repo/os/$arch
#Server = http://archlinux.ip-connect.vn.ua/$repo/os/$arch
#Server = https://archlinux.ip-connect.vn.ua/$repo/os/$arch
#Server = http://mirror.mirohost.net/archlinux/$repo/os/$arch
#Server = https://mirror.mirohost.net/archlinux/$repo/os/$arch
#Server = http://mirrors.reitarovskyi.com.ua/archlinux/$repo/os/$arch
#Server = https://mirrors.reitarovskyi.com.ua/archlinux/$repo/os/$arch
## United Arab Emirates
#Server = https://mirror.hafeezh.com/archlinux/$repo/os/$arch
## United Kingdom
#Server = http://archlinux.uk.mirror.allworldit.com/archlinux/$repo/os/$arch
#Server = https://archlinux.uk.mirror.allworldit.com/archlinux/$repo/os/$arch
#Server = https://repo.c48.uk/arch/$repo/os/$arch
#Server = https://uk.repo.c48.uk/arch/$repo/os/$arch
#Server = http://gb.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://gb.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://london.mirror.pkgbuild.com/$repo/os/$arch
#Server = http://mirror.marcusn.net/archlinux/$repo/os/$arch
#Server = https://mirror.marcusn.net/archlinux/$repo/os/$arch
#Server = http://mirrors.melbourne.co.uk/archlinux/$repo/os/$arch
#Server = https://mirrors.melbourne.co.uk/archlinux/$repo/os/$arch
#Server = http://www.mirrorservice.org/sites/ftp.archlinux.org/$repo/os/$arch
#Server = https://www.mirrorservice.org/sites/ftp.archlinux.org/$repo/os/$arch
#Server = http://mirror.netweaver.uk/archlinux/$repo/os/$arch
#Server = https://mirror.netweaver.uk/archlinux/$repo/os/$arch
#Server = http://lon.mirror.rackspace.com/archlinux/$repo/os/$arch
#Server = https://lon.mirror.rackspace.com/archlinux/$repo/os/$arch
#Server = http://mirror.server.net/archlinux/$repo/os/$arch
#Server = https://mirror.server.net/archlinux/$repo/os/$arch
#Server = https://repo.slithery.uk/$repo/os/$arch
#Server = https://mirror.st2projects.com/archlinux/$repo/os/$arch
#Server = http://mirrors.ukfast.co.uk/sites/archlinux.org/$repo/os/$arch
#Server = https://mirrors.ukfast.co.uk/sites/archlinux.org/$repo/os/$arch
#Server = http://mirror.cov.ukservers.com/archlinux/$repo/os/$arch
#Server = https://mirror.cov.ukservers.com/archlinux/$repo/os/$arch
## United States
#Server = http://mirrors.acm.wpi.edu/archlinux/$repo/os/$arch
#Server = http://mirror.adectra.com/archlinux/$repo/os/$arch
#Server = https://mirror.adectra.com/archlinux/$repo/os/$arch
#Server = https://mirror.akane.network/archmirror/$repo/os/$arch
#Server = http://arlm.tyzoid.com/$repo/os/$arch
#Server = https://arlm.tyzoid.com/$repo/os/$arch
#Server = http://ny.us.mirrors.bjg.at/arch/$repo/os/$arch
#Server = https://ny.us.mirrors.bjg.at/arch/$repo/os/$arch
#Server = http://mirrors.bloomu.edu/archlinux/$repo/os/$arch
#Server = https://mirrors.bloomu.edu/archlinux/$repo/os/$arch
#Server = https://arch-mirror.brightlight.today/$repo/os/$arch
#Server = http://mirrors.cat.pdx.edu/archlinux/$repo/os/$arch
#Server = http://us.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = https://us.mirrors.cicku.me/archlinux/$repo/os/$arch
#Server = http://mirror.clarkson.edu/archlinux/$repo/os/$arch
#Server = https://mirror.clarkson.edu/archlinux/$repo/os/$arch
#Server = http://mirror.colonelhosting.com/archlinux/$repo/os/$arch
#Server = https://mirror.colonelhosting.com/archlinux/$repo/os/$arch
#Server = http://arch.mirror.constant.com/$repo/os/$arch
#Server = https://arch.mirror.constant.com/$repo/os/$arch
#Server = http://mirror.cs.odu.edu/archlinux/$repo/os/$arch
#Server = https://mirror.cs.odu.edu/archlinux/$repo/os/$arch
#Server = http://mirror.cs.vt.edu/pub/ArchLinux/$repo/os/$arch
#Server = http://repo.customcomputercare.com/archlinux/$repo/os/$arch
#Server = https://repo.customcomputercare.com/archlinux/$repo/os/$arch
#Server = http://distro.ibiblio.org/archlinux/$repo/os/$arch
#Server = http://codingflyboy.mm.fcix.net/archlinux/$repo/os/$arch
#Server = http://coresite.mm.fcix.net/archlinux/$repo/os/$arch
#Server = http://forksystems.mm.fcix.net/archlinux/$repo/os/$arch
#Server = http://irltoolkit.mm.fcix.net/archlinux/$repo/os/$arch
#Server = http://mirror.fcix.net/archlinux/$repo/os/$arch
#Server = http://mnvoip.mm.fcix.net/archlinux/$repo/os/$arch
#Server = http://nnenix.mm.fcix.net/archlinux/$repo/os/$arch
#Server = http://nocix.mm.fcix.net/archlinux/$repo/os/$arch
#Server = http://ohioix.mm.fcix.net/archlinux/$repo/os/$arch
#Server = http://opencolo.mm.fcix.net/archlinux/$repo/os/$arch
#Server = http://southfront.mm.fcix.net/archlinux/$repo/os/$arch
#Server = http://volico.mm.fcix.net/archlinux/$repo/os/$arch
#Server = http://ziply.mm.fcix.net/archlinux/$repo/os/$arch
#Server = https://codingflyboy.mm.fcix.net/archlinux/$repo/os/$arch
#Server = https://coresite.mm.fcix.net/archlinux/$repo/os/$arch
#Server = https://forksystems.mm.fcix.net/archlinux/$repo/os/$arch
#Server = https://irltoolkit.mm.fcix.net/archlinux/$repo/os/$arch
#Server = https://mirror.fcix.net/archlinux/$repo/os/$arch
#Server = https://mnvoip.mm.fcix.net/archlinux/$repo/os/$arch
#Server = https://nnenix.mm.fcix.net/archlinux/$repo/os/$arch
#Server = https://nocix.mm.fcix.net/archlinux/$repo/os/$arch
#Server = https://ohioix.mm.fcix.net/archlinux/$repo/os/$arch
#Server = https://opencolo.mm.fcix.net/archlinux/$repo/os/$arch
#Server = https://southfront.mm.fcix.net/archlinux/$repo/os/$arch
#Server = https://volico.mm.fcix.net/archlinux/$repo/os/$arch
#Server = https://ziply.mm.fcix.net/archlinux/$repo/os/$arch
#Server = https://losangeles.mirror.pkgbuild.com/$repo/os/$arch
#Server = http://mirrors.gigenet.com/archlinux/$repo/os/$arch
#Server = https://mirror.givebytes.net/archlinux/$repo/os/$arch
#Server = https://arch.goober.cloud/$repo/os/$arch
#Server = http://arch.hu.fo/archlinux/$repo/os/$arch
#Server = https://arch.hu.fo/archlinux/$repo/os/$arch
#Server = http://arch.hugeblank.dev/$repo/os/$arch
#Server = https://arch.hugeblank.dev/$repo/os/$arch
#Server = http://repo.ialab.dsu.edu/archlinux/$repo/os/$arch
#Server = https://repo.ialab.dsu.edu/archlinux/$repo/os/$arch
#Server = http://mirrors.iu13.net/archlinux/$repo/os/$arch
#Server = https://mirrors.iu13.net/archlinux/$repo/os/$arch
#Server = https://arch.mirror.k0.ae/$repo/os/$arch
#Server = http://mirrors.kernel.org/archlinux/$repo/os/$arch
#Server = https://mirrors.kernel.org/archlinux/$repo/os/$arch
#Server = https://mirrors.lahansons.com/archlinux/$repo/os/$arch
#Server = http://mirror.sfo12.us.leaseweb.net/archlinux/$repo/os/$arch
#Server = http://mirror.wdc1.us.leaseweb.net/archlinux/$repo/os/$arch
#Server = https://mirror.sfo12.us.leaseweb.net/archlinux/$repo/os/$arch
#Server = https://mirror.wdc1.us.leaseweb.net/archlinux/$repo/os/$arch
#Server = http://mirrors.liquidweb.com/archlinux/$repo/os/$arch
#Server = http://mirrors.lug.mtu.edu/archlinux/$repo/os/$arch
#Server = https://mirrors.lug.mtu.edu/archlinux/$repo/os/$arch
#Server = https://m.lqy.me/arch/$repo/os/$arch
#Server = https://arch.mirror.marcusspencer.us:4443/archlinux/$repo/os/$arch
#Server = http://mirror.math.princeton.edu/pub/archlinux/$repo/os/$arch
#Server = http://mirror.metrocast.net/archlinux/$repo/os/$arch
#Server = http://arch.miningtcup.me/$repo/os/$arch
#Server = https://arch.miningtcup.me/$repo/os/$arch
#Server = https://mirrors.shr.cx/arch/$repo/os/$arch
#Server = http://iad.mirrors.misaka.one/archlinux/$repo/os/$arch
#Server = https://iad.mirrors.misaka.one/archlinux/$repo/os/$arch
#Server = http://repo.miserver.it.umich.edu/archlinux/$repo/os/$arch
#Server = http://mirrors.mit.edu/archlinux/$repo/os/$arch
#Server = https://mirrors.mit.edu/archlinux/$repo/os/$arch
#Server = http://mirror.mra.sh/archlinux/$repo/os/$arch
#Server = https://mirror.mra.sh/archlinux/$repo/os/$arch
#Server = https://us.arch.niranjan.co/$repo/os/$arch
#Server = http://mirrors.ocf.berkeley.edu/archlinux/$repo/os/$arch
#Server = https://mirrors.ocf.berkeley.edu/archlinux/$repo/os/$arch
#Server = http://archmirror1.octyl.net/$repo/os/$arch
#Server = https://archmirror1.octyl.net/$repo/os/$arch
#Server = http://ftp.osuosl.org/pub/archlinux/$repo/os/$arch
#Server = https://ftp.osuosl.org/pub/archlinux/$repo/os/$arch
#Server = https://mirror.pilotfiber.com/archlinux/$repo/os/$arch
#Server = http://dfw.mirror.rackspace.com/archlinux/$repo/os/$arch
#Server = http://iad.mirror.rackspace.com/archlinux/$repo/os/$arch
#Server = http://ord.mirror.rackspace.com/archlinux/$repo/os/$arch
#Server = https://dfw.mirror.rackspace.com/archlinux/$repo/os/$arch
#Server = https://iad.mirror.rackspace.com/archlinux/$repo/os/$arch
#Server = https://ord.mirror.rackspace.com/archlinux/$repo/os/$arch
#Server = http://plug-mirror.rcac.purdue.edu/archlinux/$repo/os/$arch
#Server = https://plug-mirror.rcac.purdue.edu/archlinux/$repo/os/$arch
#Server = http://mirrors.rit.edu/archlinux/$repo/os/$arch
#Server = https://mirrors.rit.edu/archlinux/$repo/os/$arch
#Server = http://mirror.siena.edu/archlinux/$repo/os/$arch
#Server = http://mirrors.smeal.xyz/arch-linux/$repo/os/$arch
#Server = https://mirrors.smeal.xyz/arch-linux/$repo/os/$arch
#Server = http://mirrors.sonic.net/archlinux/$repo/os/$arch
#Server = https://mirrors.sonic.net/archlinux/$repo/os/$arch
#Server = https://us-mnz.soulharsh007.dev/archlinux/$repo/os/$arch
#Server = http://mirror.pit.teraswitch.com/archlinux/$repo/os/$arch
#Server = https://mirror.pit.teraswitch.com/archlinux/$repo/os/$arch
#Server = https://mirror.theash.xyz/arch/$repo/os/$arch
#Server = http://mirror.umd.edu/archlinux/$repo/os/$arch
#Server = https://mirror.umd.edu/archlinux/$repo/os/$arch
#Server = http://mirrors.vectair.net/archlinux/$repo/os/$arch
#Server = https://mirrors.vectair.net/archlinux/$repo/os/$arch
#Server = http://mirror.vtti.vt.edu/archlinux/$repo/os/$arch
#Server = http://wcbmedia.io:8000/$repo/os/$arch
#Server = http://mirrors.xmission.com/archlinux/$repo/os/$arch
#Server = http://mirrors.xtom.com/archlinux/$repo/os/$arch
#Server = https://mirrors.xtom.com/archlinux/$repo/os/$arch
#Server = https://yonderly.org/mirrors/archlinux/$repo/os/$arch
#Server = https://mirror.zackmyers.io/archlinux/$repo/os/$arch
#Server = https://zxcvfdsa.com/arch/$repo/os/$arch
## Uzbekistan
#Server = http://mirror.dc.uz/arch/$repo/os/$arch
#Server = https://mirror.dc.uz/arch/$repo/os/$arch
## Vietnam
#Server = http://mirror.bizflycloud.vn/archlinux/$repo/os/$arch
#Server = https://mirrors.huongnguyen.dev/arch/$repo/os/$arch
#Server = https://mirror.meowsmp.net/arch/$repo/os/$arch
#Server = https://mirrors.nguyenhoang.cloud/archlinux/$repo/os/$arch
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# Stormux accessibility environment variables
# Available to all users and sessions
# Accessibility support
export ACCESSIBILITY_ENABLED=1
export GTK_MODULES=gail:atk-bridge:canberra-gtk-module
export GNOME_ACCESSIBILITY=1
export QT_ACCESSIBILITY=1
export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1
# Dialog accessibility options
export DIALOGOPTS='--no-lines --visit-items'
@@ -51,11 +51,7 @@ clear
cat << "EOF"
Hello, and welcome to Stormux!
Let's get you set up. After you press enter, you will be prompted for the sudo password.
When that happens, type the word stormux and press enter.
You will not receive any speech feedback for this process.
That is completely normal, and speech will return after you have typed the password.
Once again, the password is stormux in all lower case letters.
Let's get you set up.
Please press enter to continue.
EOF
+43
View File
@@ -0,0 +1,43 @@
#
# ~/.bashrc
#
# If not running interactively, don't do anything
[[ $- != *i* ]] && return
# Change directories without using cd
shopt -s autocd
# Load aliases and functions if they exist
[[ -f "$HOME/.bash_aliases" ]] && . "$HOME/.bash_aliases"
[[ -f "$HOME/.bash_functions" ]] && . "$HOME/.bash_functions"
# Environment variables
export QT_ACCESSIBILITY=1
export QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1
# Set prompt
PS1='[\u@\h \W] \$ '
# Dialog accessibility options
export DIALOGOPTS='--no-lines --visit-items'
# Browser selection based on environment
if [[ -z "$DISPLAY" ]]; then
export BROWSER=w3m
fi
# GPG TTY
GPG_TTY=$(tty)
export GPG_TTY
# Don't put commands prefixed with space, or duplicate commands in history
export HISTCONTROL=ignoreboth
# Increase history size
export HISTSIZE=10000
export HISTFILESIZE=20000
# Add user's local bin directories to PATH
PATH="$HOME/.local/bin:$HOME/bin:$PATH"
export PATH
+25
View File
@@ -0,0 +1,25 @@
# ~/.inputrc
# Readline configuration for bash
# Reload changes with control+x followed by control+r
# Don't echo control characters
set echo-control-characters off
# History searching with up and down arrows
"\e[A": history-search-backward
"\e[B": history-search-forward
# Shift+Tab sequences for console (backward menu completion)
"\e[Z": menu-complete-backward
"\eZ": menu-complete-backward
# Alt+comma as alternative
"\e,": menu-complete-backward
# Music player keybindings (requires playerctl)
"\eX": "\C-k \C-u playerctl play\C-M"
"\eC": "\C-k \C-u playerctl play-pause\C-M"
"\eV": "\C-k \C-u playerctl stop\C-M"
"\eB": "\C-k \C-u playerctl next\C-M"
"\eU": "\C-k \C-u playerctl metadata -f '{{artist}} - {{album}} - {{title}}'\C-M"
"\e_": "\C-k \C-u playerctl volume 0.05-\C-M"
"\e+": "\C-k \C-u playerctl volume 0.05+\C-M"
@@ -0,0 +1,33 @@
# ~/.screenrc
# GNU Screen configuration
# Disable visual bell
vbell off
bell_msg ""
# Disable hardstatus
hardstatus off
# Disable startup message
startup_message off
# Set scrollback buffer size
defscrollback 4096
# Extended window numbering (0-19 instead of 0-9)
bind ! select 10
bind @ select 11
bind \# select 12
bind $ select 13
bind % select 14
bind ^ select 15
bind & select 16
bind * select 17
bind ( select 18
bind ) select 19
# Copy buffer to system clipboard (requires xclip)
bind b eval "writebuf" 'exec !!! xclip -selection "clipboard" -i /tmp/screen-exchange'
# Disable alternate screen switching (allows scrollback in terminal)
termcapinfo xterm* ti@:te@
@@ -0,0 +1,29 @@
" Date and time insertion functions
" Insert current date in format: Wednesday, January 15, 2025
function! InsertDate()
let date_string = strftime("%A, %B %d, %Y")
execute "normal! a" . date_string
endfunction
" Insert current time in format: 09:15PM
function! InsertTime()
let time_string = strftime("%I:%M%p")
execute "normal! a" . time_string
endfunction
" Insert current date and time in format: Wednesday, January 15, 2025, 09:15PM
function! InsertDateTime()
let datetime_string = strftime("%A, %B %d, %Y, %I:%M%p")
execute "normal! a" . datetime_string
endfunction
" Key mappings
nnoremap <leader>date :call InsertDate()<CR>
inoremap <leader>date <C-o>:call InsertDate()<CR>
nnoremap <leader>time :call InsertTime()<CR>
inoremap <leader>time <C-o>:call InsertTime()<CR>
nnoremap <leader>datetime :call InsertDateTime()<CR>
inoremap <leader>datetime <C-o>:call InsertDateTime()<CR>
@@ -0,0 +1,39 @@
" Emoji insertion mappings
" Basic expressions
inoremap ,:) 😊
inoremap ,:( 😢
inoremap ,:D 😃
inoremap ,;) 😉
inoremap ,:P 😛
inoremap ,:o 😮
inoremap ,:\| 😐
" Hearts and love
inoremap ,<3
inoremap ,heart
inoremap ,love 💕
inoremap ,kiss 😘
" Special expressions
inoremap ,>:) 😈
inoremap ,devil 😈
inoremap ,:* 😘
inoremap ,cool 😎
inoremap ,nerd 🤓
inoremap ,angry 😠
" Hands and gestures
inoremap ,thumbs 👍
inoremap ,clap 👏
inoremap ,wave 👋
inoremap ,peace
inoremap ,pray 🙏
" Common symbols
inoremap ,check
inoremap ,x
inoremap ,star
inoremap ,fire 🔥
inoremap ,rocket 🚀
inoremap ,tada 🎉
+77
View File
@@ -0,0 +1,77 @@
" ~/.vimrc
" Vim configuration for Stormux
" Ensure vim directory structure exists
" mkdir -p ~/.vim/{backup,swap,views}
set nocompatible " Use Vim settings (not Vi compatible)
filetype off " Required for some plugins
" Terminal and encoding
set term=linux " Make arrow and other keys work
scriptencoding utf-8
" History and undo
set history=1000 " Store more history (default is 20)
set undoreload=10000 " Maximum lines to save for undo on buffer reload
" Backup and swap file configuration
set backup " Enable backups
set backupdir=$HOME/.vim/backup//
set directory=$HOME/.vim/swap//
set viewdir=$HOME/.vim/views//
" Search settings
set ignorecase " Case insensitive search
set smartcase " Case sensitive when uppercase present
set gdefault " /g flag on :s substitutions by default
" Spell checking
set spell " Enable spell checking
" Display settings
set wrap " Wrap long lines
set linebreak " Break lines at word boundaries
set noruler " Disable ruler (for accessibility)
set laststatus=0 " Never show status line
set noshowcmd " Don't show commands in status
" Indentation settings
set shiftwidth=4 " Use indents of 4 spaces
set expandtab " Tabs are spaces, not tabs
set tabstop=4 " An indentation every four columns
set softtabstop=4 " Let backspace delete indent
" Leader key
let mapleader = ','
" Remap semicolon to colon for easier commands
nnoremap ; :
" Yank from cursor to end of line (consistent with C and D)
nnoremap Y y$
" Em-dash insertion
inoremap ,-
" Working directory shortcuts
cmap cwd lcd %:p:h
cmap cd. lcd %:p:h
" Write with sudo if you forgot to open with sudo
cmap w!! w !sudo tee % >/dev/null
" Disable automatic comment continuation
autocmd FileType * setlocal formatoptions-=c formatoptions-=r formatoptions-=o
" Load vim helper functions if they exist
if filereadable(expand("~/.vim/dates.vim"))
source ~/.vim/dates.vim
endif
if filereadable(expand("~/.vim/emoji.vim"))
source ~/.vim/emoji.vim
endif
" Enable filetype detection and indenting
filetype plugin indent on
@@ -0,0 +1,7 @@
# SSH configuration for Stormux Live ISO
# This configuration allows root login for the live environment
# Users should change the root password and disable this after installation
PermitRootLogin yes
PasswordAuthentication yes
ChallengeResponseAuthentication no
@@ -0,0 +1,15 @@
# Stormux Assistance System - Client Configuration
[server]
host = assistance.stormux.org
ssh_port = 22
ssh_user = stormux-assist
tunnel_port = 2222
[session]
# Session timeout in seconds (14400 = 4 hours)
timeout = 14400
[client]
log_file = /var/log/sas.log
log_dir = ~/stormux-assist-logs/
@@ -0,0 +1 @@
../ssh-login-monitor.service
@@ -0,0 +1 @@
/usr/lib/systemd/system/sshd.socket
@@ -0,0 +1,17 @@
[Unit]
Description=Fenrir SSH Login Monitor
After=sshd.service
Wants=sshd.service
[Service]
Type=simple
ExecStart=/usr/share/fenrirscreenreader/scripts/ssh-login-monitor.sh
Restart=on-failure
RestartSec=5
# Security settings
NoNewPrivileges=true
PrivateTmp=false
[Install]
WantedBy=multi-user.target
@@ -0,0 +1 @@
SUBSYSTEM=="tty", KERNEL=="tty[0-9]*|hvc[0-9]*|sclp_line[0-9]*|ttysclp[0-9]*|3270/tty[0-9]*", GROUP="tty", MODE="0620"
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
#
# Archiso airootfs customization script
# This runs in the airootfs chroot during ISO build
set -e -u
# Initialize pacman keyring
echo "Initializing pacman keyring..."
pacman-key --init
pacman-key --populate archlinux
# Add Stormux repository key
echo "Adding Stormux repository key..."
if [ -f /root/stormux_repo.pub ]; then
pacman-key --add /root/stormux_repo.pub
pacman-key --lsign-key 52ADA49000F1FF0456F8AEEFB4CDE1CD56EF8E82
echo "Stormux repository key added and locally signed"
rm /root/stormux_repo.pub
else
echo "Warning: Stormux repository key not found at /root/stormux_repo.pub"
fi
# Set locale
echo "en_US.UTF-8 UTF-8" > /etc/locale.gen
locale-gen
# Enable system services
systemctl enable NetworkManager.service
systemctl enable fenrirscreenreader.service
systemctl enable brltty.path
# Enable user services globally for all users
systemctl --global enable pipewire.service pipewire-pulse.service
# Set permissions for stormux user home
if [ -d /home/stormux ]; then
chown -R 1000:1000 /home/stormux
fi
# Copy Stormux custom skel files to /etc/skel
# This is done after package installation to avoid conflicts with packages like screen
echo "Copying Stormux custom skel files..."
if [ -d /etc/skel.stormux ]; then
cp -a /etc/skel.stormux/. /etc/skel/
echo "Stormux skel files copied to /etc/skel"
else
echo "Warning: /etc/skel.stormux not found"
fi
echo "Airootfs customization complete"
+29
View File
@@ -0,0 +1,29 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGhTtIcBEADTCqY/+das1n52HTTlp3Uf5+ER5r0fNA2HgJq4GzEeFBdqdgSF
y05P9B94d4JWtWvt0ckjsNfku19O9IquKiYP5x9/sYT8RGeRUrTlN9qlCrytTsdM
a8ij9esGQOXDeyRJQTs5igbFWQ26MrJpOkmO8xMXGmfD29yOppRLzVS8Jed0mokk
aEajui3QSWcefDnFMBYNaMF04iuWgVxv/7102adh3u3+V632+fQQhFtu+x92iosU
GtWlGGe3UkEhWJIb6wf/VCYIYQAjxTQBLul2WByXqwCXmobzc7DJbte3FSEPvPis
nJiUmT0fsIIC9c1UbTJt5Gqb6o/oI61tEJEeXieBViXFE7HwBI7FUxaIXoozgiYA
BxaLHQT0Qv9k1SXbBm82xNydjqFHIAolYSp5s1wRIhErlOaG9w3+Tb8zFSekPsbg
cgS/b3PpWmnigXBJQ94Ee+6KoPMVrwEKIu/IHexRUVAm+66Xs55ccgMY+MUupIjX
Bdn9jCYB8NwpC3UNWoSv2oQ7F2hVbXZYC/dx7gKmyvzfrw5MLOX7yEoSGlJJp2iK
fLLP8nw+x7LnDFEnAjPjtmeH7e+JANH1scP/7YMKDszO1THSK3hBAK1aeq0aEFs8
9AO++xZrNbCrF2MnqcGaXs/753k6kf8zyrW6t7A1opxjPHIz3WrQ8SbXXwARAQAB
tExTdG9ybXV4IChGb3Igc2lnbmluZyBzdG9ybXV4IHJlcG9zaXRvcnkgcGFja2Fn
ZXMpIDxzdG9ybV9kcmFnb25Ac3Rvcm11eC5vcmc+iQJOBBMBCgA4FiEEUq2kkADx
/wRW+K7vtM3hzVbvjoIFAmhTtIcCGwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AA
CgkQtM3hzVbvjoLiLQ/9HJzlwr2JNGdR/LaM1Y6VuhASGDZsUsi5XLJ7qKUKN/eA
kq0LMO7TzHO++otsEolodMr8fTkthQTPqSnbLZsW9Y3wK9U2vajryblRSEBaY6VB
1ds0pAg5SXz7O2eI2Ne05dkYB176G72KcjrOiSanKjI9sqh6pdTHcQM3XbRxxEVn
or7fKbLs/eveFWRPW3EAwuVnWI1aw3Xw9BQ3TE1eEDPvBh2GVKZ6qskbzdwfZtUB
kxAa30M+ftlNDaV7d6mNfREefEJfZjYFrY2XiqTK3+w1Q1ZaugK0Bbd/YdAoGMdt
6gbd2h/IZtxduWW/HhvWX2wtKqn4fNxAyYcY3aWm8kuq0pXlyEZ96gZas6a2lu+f
+yLvIh3UtC1IimFMDMQnKCTbWgdhd0HMnG9ULnoOroXVGltnBQwlO0hrHSVD+qmy
NVPUxkzS3ZhqSmoW2G8hSvXpMjt/w5fhC0FJhFb/uM/7LqJWSl5aAkUHupN3t/nG
9o7chBQzzXwbpiS6DQuIt6ouyPpwNjeELFPNBjZDf0auVWj6ZZ5+B/bmyHXQ4wFS
bYKZdO9VeXhBxRUMfGpIKvIw/Lnc+1Toan1RTWoUoRH+HuK2D/LAuvsam36gtLrQ
XV3//7Mh4oQ0Zg4REG3SEfG9i3rav8wMtRmaJaSbwmMVpC6mT/uiEzCSgQhO/0c=
=HIM9
-----END PGP PUBLIC KEY BLOCK-----
+14 -75
View File
@@ -1,29 +1,10 @@
#!/usr/bin/env bash
# For audible sudo prompts:
unset sudoFlags
if [[ -x /etc/audibleprompt.sh ]]; then
export SUDO_ASKPASS=/etc/audibleprompt.sh
export sudoFlags=("-A")
fi
trap cleanup EXIT
cleanup() {
popd &> /dev/null
if ! [[ -x /opt/configure-stormux/configure-stormux.sh ]]; then
echo "Initial setup is not complete."
echo "To continue setup, please run:"
echo "sudo configure-stormux"
fi
echo "Thank you for choosing Stormux!"
}
if [[ -x /opt/configure-stormux/configure-stormux.sh ]]; then
pushd /opt/configure-stormux
./configure-stormux.sh
exit 0
fi
# Volume calibration is now handled in stormux_first_boot.sh
export DIALOGOPTS='--insecure --no-lines --visit-items'
@@ -36,7 +17,7 @@ set_timezone() {
region=$(dialog --backtitle "Please select your Region" \
--no-tags \
--menu "Use up and down arrows or page-up and page-down to navigate the list, and press 'Enter' to make your selection." 0 0 0 \
$(for i in ${regions[@]} ; do echo "$i";echo "$i";done) --stdout)
$(for i in "${regions[@]}" ; do echo "$i";echo "$i";done) --stdout)
mapfile -t cities < <(timedatectl --no-pager list-timezones | grep "$region" | cut -d '/' -f2 | sort -u)
@@ -45,13 +26,13 @@ set_timezone() {
city=$(dialog --backtitle "Please select a city near you" \
--no-tags \
--menu "Use up and down arrow or page-up and page-down to navigate the list." 0 0 10 \
$(for i in ${cities[@]} ; do echo "$i";echo "$i";done) --stdout)
$(for i in "${cities[@]}" ; do echo "$i";echo "$i";done) --stdout)
# Set the timezone
if [[ -f /etc/localtime ]]; then
rm /etc/localtime
fi
ln -sf /usr/share/zoneinfo/${region}/${city} /etc/localtime
ln -sf /usr/share/zoneinfo/"${region}"/"${city}" /etc/localtime
timedatectl set-ntp true
}
# Offer to switch fenrir layout.
@@ -65,29 +46,6 @@ if [[ "${continue,}" == "y" ]];then
systemctl restart fenrirscreenreader.service
fi
# Check for possible resize
diskSource="$(df --output='source' / | tail -1)"
diskSize="$(df -h --output='size' / | tail -1 | tr -cd '[:digit:].')"
diskSize=${diskSize%.*}
if [[ $diskSize -le 7 ]]; then
echo "$diskSource is only $diskSize gigs, which means it probably needs to be resized. Would you like to do this now?"
echo "Press y for yes or n for no followed by enter."
read -r continue
continue="${continue::1}"
if [[ "${continue,}" == "y" ]];then
# Extract base device name (handles mmcblk0p2, nvme0n1p2, sda2, etc.)
if [[ "$diskSource" =~ (.*[0-9]+)p[0-9]+$ ]]; then
# Handle mmcblk0p2, nvme0n1p2 style
diskDevice="${BASH_REMATCH[1]}"
else
# Handle sda2, sdb3 style
diskDevice="$(echo "$diskSource" | sed 's/[0-9]*$//')"
fi
echo "Yes" | sudo "${sudoFlags[@]}" parted ---pretend-input-tty "$diskDevice" resizepart 2 100%
sudo "${sudoFlags[@]}" resize2fs -f "$diskSource"
fi
fi
if ! ping -c1 stormux.org &> /dev/null ; then
echo "No internet connection detected. Press enter to open NetworkManager."
read -r continue
@@ -103,39 +61,20 @@ if ping -qc1 -W 1 stormux.org &> /dev/null; then
echo "Current date and time: $date_time"
# set date and time
date -s "$date_time"
echo "If your Pi does not have a CMOS battery and is powered off regularly, it may take a while for the time to be set correctly after boot."
echo "Stormux provides a service to sync the time after internet connection is established."
read -rp "Would you like the time to sync as soon as the Pi connects to the internet? " answer
answer="${answer:0:1}"
if [[ "${answer,,}" == "y" ]]; then
systemctl enable time_sync_at_boot.service
else
echo "Time sync at boot skipped."
echo "If you change your mind later, simply type:"
echo "sudo systemctl enable time_sync_at_boot.service"
fi
set_timezone
else
echo "Please connect to the internet and run ${0##*/} again."
exit 1
fi
echo "Installing configure-stormux..."
git -C /opt clone -q https://git.stormux.org/storm/configure-stormux || exit 1
echo
echo "Initial setup is complete."
echo
echo "The default passwords are stormux for the stormux user"
echo "and root for the root user. It is highly recommended to change them."
echo "To change the password for stormux, run:"
echo "passwd"
echo "To change the password for root, run:"
echo "sudo passwd"
echo
echo "For more configuration options, run configure-stormux,"
echo "or you may configure your system manually."
echo
echo "Thank you for choosing Stormux."
exit 0
read -rp "Would you like to run the Stormux installer? [y/N]: " answer
answer="${answer:0:1}"
if [[ "${answer,,}" == "y" ]]; then
install-stormux
else
echo
echo "To run the Stormux installer, type install-stormux"
echo
exit 0
fi
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
sas.sh
+582
View File
@@ -0,0 +1,582 @@
#!/usr/bin/env python3
import os
import re
import secrets
import signal
import string
import subprocess
import sys
import tempfile
import time
import pwd
import threading
stormuxAdmin = ("storm",)
ircServer = "irc.stormux.org"
ircPort = 6667
ircChannel = "#stormux"
remoteHost = "billysballoons.com"
sasUser = "sas"
pingIntervalSeconds = 180
pingCount = 5
maxWormholeFailures = 3
sudoKeepaliveThread = None
sudoKeepaliveStop = threading.Event()
def speak_message(message):
try:
subprocess.run(["spd-say", message], check=False)
except FileNotFoundError:
print(message, flush=True)
def say_or_print(message, useSpeech):
if useSpeech:
speak_message(message)
else:
print(message, flush=True)
def run_command(command, inputText=None, check=False, env=None):
return subprocess.run(
command,
input=inputText,
text=True,
capture_output=True,
check=check,
env=env,
)
def ensure_sudo(useSpeech):
if os.geteuid() == 0:
return True
if useSpeech:
speak_message("Sudo password required. Please enter your password now.")
result = run_command(["sudo", "-v"])
if result.returncode == 0:
start_sudo_keepalive()
return True
return False
def start_sudo_keepalive():
global sudoKeepaliveThread
if sudoKeepaliveThread and sudoKeepaliveThread.is_alive():
return
def keepalive_loop():
while not sudoKeepaliveStop.wait(240):
run_command(["sudo", "-n", "-v"])
sudoKeepaliveThread = threading.Thread(target=keepalive_loop, daemon=True)
sudoKeepaliveThread.start()
def run_privileged(command, useSpeech, inputText=None, check=True):
if os.geteuid() == 0:
fullCommand = command
else:
if not ensure_sudo(useSpeech):
raise RuntimeError("sudo authentication failed")
fullCommand = ["sudo"] + command
return run_command(fullCommand, inputText=inputText, check=check)
def run_as_user(userName, command, useSpeech, check=True):
if os.geteuid() == 0:
fullCommand = ["sudo", "-u", userName, "-H"] + command
else:
if not ensure_sudo(useSpeech):
raise RuntimeError("sudo authentication failed")
fullCommand = ["sudo", "-u", userName, "-H"] + command
return run_command(fullCommand, check=check)
def user_exists(userName):
result = run_command(["getent", "passwd", userName])
return result.returncode == 0
def ensure_wheel(userName, useSpeech):
result = run_command(["id", "-nG", userName])
groups = result.stdout.strip().split()
if "wheel" not in groups:
run_privileged(["usermod", "-a", "-G", "wheel", userName], useSpeech)
def generate_password():
allowedChars = string.ascii_letters + string.digits
length = secrets.randbelow(5) + 6
return "".join(secrets.choice(allowedChars) for _ in range(length))
def get_user_home(userName):
return pwd.getpwnam(userName).pw_dir
def set_password(userName, password, useSpeech):
run_privileged(["chpasswd"], useSpeech, inputText=f"{userName}:{password}\n")
def generate_ssh_key(userName, useSpeech):
userHome = get_user_home(userName)
sshDir = os.path.join(userHome, ".ssh")
privateKeyPath = os.path.join(sshDir, "id_ed25519")
publicKeyPath = f"{privateKeyPath}.pub"
run_privileged(["mkdir", "-p", sshDir], useSpeech)
run_privileged(["chown", f"{userName}:{userName}", sshDir], useSpeech)
run_privileged(["chmod", "700", sshDir], useSpeech)
for entry in list_subdirs(sshDir):
if os.path.isfile(entry) or os.path.islink(entry):
run_privileged(["rm", "-f", entry], useSpeech, check=False)
run_as_user(
userName,
["ssh-keygen", "-t", "ed25519", "-N", "", "-f", privateKeyPath],
useSpeech,
)
run_privileged(["chmod", "600", privateKeyPath], useSpeech)
run_privileged(["chmod", "644", publicKeyPath], useSpeech)
run_privileged(["chown", f"{userName}:{userName}", privateKeyPath, publicKeyPath], useSpeech)
return privateKeyPath, publicKeyPath
def path_exists_for_user(path, userName, useSpeech):
result = run_as_user(userName, ["stat", path], useSpeech, check=False)
return result.returncode == 0
def extract_message_text(line):
if "> " in line:
return line.split("> ", 1)[1].strip()
if ": " in line:
return line.split(": ", 1)[1].strip()
return line.strip()
def parse_sender(line):
match = re.search(r"<([^>]+)>", line)
if match:
return match.group(1)
return None
def find_wormhole_code(message):
lowered = message.strip().lower()
if lowered in ("yes", "accept"):
return None
match = re.search(r"\b\d+-[A-Za-z0-9-]+\b", message)
if match:
return match.group(0)
return None
class IrcSession:
def __init__(self, server, port, nick, channel, baseDir):
self.server = server
self.port = port
self.nick = nick
self.channel = channel
self.baseDir = baseDir
self.serverDir = None
self.serverInPath = None
self.channelInPath = None
self.iiProcess = None
self.pmOffsets = {}
def start(self):
if not shutil_which("ii"):
raise RuntimeError("ii is not installed")
supportsI = ii_supports_i()
processEnv = os.environ.copy()
iiCommand = ["ii", "-s", self.server, "-p", str(self.port), "-n", self.nick]
if supportsI:
iiCommand += ["-i", self.baseDir]
else:
processEnv["HOME"] = self.baseDir
self.iiProcess = subprocess.Popen(
iiCommand,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=processEnv,
)
self.serverDir = self.wait_for_server_dir()
self.serverInPath = os.path.join(self.serverDir, "in")
def stop(self):
if self.channelInPath and os.path.exists(self.channelInPath):
try:
self.write_line(self.channelInPath, f"/part {self.channel}")
except OSError:
pass
if self.serverInPath and os.path.exists(self.serverInPath):
try:
self.write_line(self.serverInPath, "/quit")
except OSError:
pass
if self.iiProcess and self.iiProcess.poll() is None:
self.iiProcess.terminate()
try:
self.iiProcess.wait(timeout=5)
except subprocess.TimeoutExpired:
self.iiProcess.kill()
def join_channel(self):
joinMessage = f"/join {self.channel}"
channelDir = os.path.join(self.serverDir, self.channel)
channelAltDir = os.path.join(self.serverDir, self.channel.lstrip("#"))
startTime = time.monotonic()
nextJoinTime = startTime
while time.monotonic() - startTime < 60:
if time.monotonic() >= nextJoinTime:
self.write_line(self.serverInPath, joinMessage)
nextJoinTime = time.monotonic() + 5
for candidate in (channelDir, channelAltDir):
inPath = os.path.join(candidate, "in")
if os.path.exists(inPath):
self.channelInPath = inPath
return
time.sleep(0.5)
self.channelInPath = None
def send_channel_message(self, message):
if not self.channelInPath:
self.refresh_channel_in_path()
if self.channelInPath and os.path.exists(self.channelInPath):
self.write_line(self.channelInPath, message)
else:
self.write_line(self.serverInPath, f"/msg {self.channel} {message}")
def send_private_message(self, nick, message):
nickDir = os.path.join(self.serverDir, nick)
inPath = os.path.join(nickDir, "in")
if os.path.exists(inPath):
self.write_line(inPath, message)
else:
self.write_line(self.serverInPath, f"/msg {nick} {message}")
def get_private_messages(self, allowedUsers):
messages = []
for nick in allowedUsers:
nickDir = os.path.join(self.serverDir, nick)
outPath = os.path.join(nickDir, "out")
if not os.path.exists(outPath):
continue
lastPos = self.pmOffsets.get(outPath, 0)
with open(outPath, "r", encoding="utf-8", errors="ignore") as fileHandle:
fileHandle.seek(lastPos)
for line in fileHandle:
sender = parse_sender(line)
if sender and sender == self.nick:
continue
if sender and sender != nick:
continue
messageText = extract_message_text(line)
if messageText:
messages.append((nick, messageText))
self.pmOffsets[outPath] = fileHandle.tell()
return messages
def refresh_channel_in_path(self):
channelDir = os.path.join(self.serverDir, self.channel)
channelAltDir = os.path.join(self.serverDir, self.channel.lstrip("#"))
for candidate in (channelDir, channelAltDir):
inPath = os.path.join(candidate, "in")
if os.path.exists(inPath):
self.channelInPath = inPath
return
def wait_for_server_dir(self):
for _ in range(120):
for rootDir in [self.baseDir] + list_subdirs(self.baseDir):
if not os.path.isdir(rootDir):
continue
for entry in os.listdir(rootDir):
path = os.path.join(rootDir, entry)
if os.path.isdir(path) and self.server in entry:
inPath = os.path.join(path, "in")
if os.path.exists(inPath):
return path
time.sleep(0.5)
raise RuntimeError("ii server directory not found")
@staticmethod
def write_line(path, message):
with open(path, "w", encoding="utf-8", errors="ignore") as fileHandle:
fileHandle.write(message + "\n")
fileHandle.flush()
def ii_supports_i():
result = run_command(["ii", "-h"])
output = (result.stdout or "") + (result.stderr or "")
return "-i" in output
def list_subdirs(path):
try:
return [os.path.join(path, entry) for entry in os.listdir(path)]
except OSError:
return []
def shutil_which(command):
for path in os.environ.get("PATH", "").split(os.pathsep):
candidate = os.path.join(path, command)
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
return candidate
return None
def build_nick():
baseUser = os.environ.get("SUDO_USER") or os.environ.get("USER") or "sas"
return f"{baseUser}-{int(time.time())}"
def main():
say_or_print("Checking accessibility. Is your screen reader working? (y/n)", True)
answer = input().strip().lower()
useSpeech = answer in ("n", "no")
shouldRemoveUser = False
cleanupDone = False
tempDir = tempfile.mkdtemp(prefix="sas-ii-")
ircSession = None
sshProcess = None
def cleanup(exitMessage=None):
nonlocal cleanupDone
if cleanupDone:
return
cleanupDone = True
nonlocal sshProcess
if exitMessage:
say_or_print(exitMessage, useSpeech)
if sshProcess and sshProcess.poll() is None:
sshProcess.terminate()
try:
sshProcess.wait(timeout=5)
except subprocess.TimeoutExpired:
sshProcess.kill()
if ircSession:
ircSession.stop()
if shouldRemoveUser:
try:
run_privileged(["pkill", "-u", sasUser], useSpeech, check=False)
time.sleep(1)
result = run_privileged(["userdel", "-r", sasUser], useSpeech, check=False)
run_privileged(["rm", "-rf", f"/home/{sasUser}"], useSpeech, check=False)
if result.returncode != 0 and user_exists(sasUser):
say_or_print(
"Cleanup warning: failed to remove sas user. Please remove it manually.",
useSpeech,
)
except Exception:
pass
sudoKeepaliveStop.set()
if sudoKeepaliveThread:
sudoKeepaliveThread.join(timeout=2)
try:
remove_tree(tempDir)
except Exception:
pass
def handle_signal(signum, frame):
cleanup("Interrupted. Cleaning up.")
sys.exit(1)
signal.signal(signal.SIGINT, handle_signal)
signal.signal(signal.SIGTERM, handle_signal)
try:
if not user_exists(sasUser):
run_privileged(
["useradd", "-m", "-d", f"/home/{sasUser}", "-s", "/bin/bash", "-G", "wheel", sasUser],
useSpeech,
)
shouldRemoveUser = True
else:
say_or_print(
"User 'sas' exists. Remove and recreate it? This will delete /home/sas. (y/n)",
useSpeech,
)
response = input().strip().lower()
if response not in ("y", "yes"):
cleanup("The sas user is unavailable. Remove it manually and try again.")
return 1
run_privileged(["pkill", "-u", sasUser], useSpeech, check=False)
run_privileged(["userdel", "-r", sasUser], useSpeech, check=False)
run_privileged(["rm", "-rf", f"/home/{sasUser}"], useSpeech, check=False)
run_privileged(
["useradd", "-m", "-d", f"/home/{sasUser}", "-s", "/bin/bash", "-G", "wheel", sasUser],
useSpeech,
)
shouldRemoveUser = True
ensure_wheel(sasUser, useSpeech)
password = generate_password()
set_password(sasUser, password, useSpeech)
privateKeyPath, publicKeyPath = generate_ssh_key(sasUser, useSpeech)
sasHome = get_user_home(sasUser)
knownHostsPath = os.path.join(sasHome, ".ssh", "known_hosts_sas")
run_privileged(["touch", knownHostsPath], useSpeech)
run_privileged(["chmod", "600", knownHostsPath], useSpeech)
run_privileged(["chown", f"{sasUser}:{sasUser}", knownHostsPath], useSpeech)
nick = build_nick()
ircSession = IrcSession(ircServer, ircPort, nick, ircChannel, tempDir)
ircSession.start()
ircSession.join_channel()
say_or_print("Waiting for assistance on IRC.", useSpeech)
startTime = time.monotonic()
nextPingTime = startTime
pingsSent = 0
confirmedAdmin = None
while time.monotonic() - startTime < pingIntervalSeconds * pingCount:
now = time.monotonic()
if pingsSent < pingCount and now >= nextPingTime:
ircSession.send_channel_message(f"{nick} is requesting assistance.")
pingsSent += 1
nextPingTime = startTime + (pingsSent * pingIntervalSeconds)
for adminNick, messageText in ircSession.get_private_messages(stormuxAdmin):
if messageText.strip().lower() in ("yes", "accept"):
confirmedAdmin = adminNick
break
if confirmedAdmin:
break
time.sleep(1)
if not confirmedAdmin:
cleanup("No one was available to help, please try again later.")
return 1
ircSession.send_private_message(
confirmedAdmin,
f'password: "{password}" please send wormhole ssh invite code',
)
failures = 0
while failures < maxWormholeFailures:
inviteCode = None
while inviteCode is None:
for adminNick, messageText in ircSession.get_private_messages(stormuxAdmin):
inviteCode = find_wormhole_code(messageText)
if inviteCode:
break
if inviteCode:
break
time.sleep(1)
if not path_exists_for_user(publicKeyPath, sasUser, useSpeech):
raise RuntimeError(f"Public key missing: {publicKeyPath}")
wormholeCommand = [
"wormhole",
"ssh",
"accept",
"--yes",
inviteCode,
]
result = run_as_user(sasUser, wormholeCommand, useSpeech, check=False)
if result.returncode == 0:
say_or_print("Wormhole key transfer succeeded.", useSpeech)
break
failures += 1
errorTextFull = (result.stderr or result.stdout or "").strip()
if errorTextFull and not useSpeech:
print("Wormhole ssh accept error:", flush=True)
print(errorTextFull, flush=True)
errorText = errorTextFull
if errorText:
errorText = " ".join(errorText.split())
if len(errorText) > 400:
errorText = errorText[:400] + "..."
ircSession.send_private_message(
confirmedAdmin,
f"Wormhole ssh accept failed: {errorText}",
)
ircSession.send_private_message(
confirmedAdmin,
"Wormhole ssh accept failed. Please send a new invite code.",
)
if failures >= maxWormholeFailures:
cleanup("Wormhole failed too many times. Exiting.")
return 1
say_or_print("Starting reverse SSH tunnel. Press Ctrl+C to stop.", useSpeech)
sshCommand = [
"ssh",
"-N",
"-R",
"localhost:2232:localhost:22",
"-o",
"ExitOnForwardFailure=yes",
"-o",
"BatchMode=yes",
"-o",
"ServerAliveInterval=30",
"-o",
"ServerAliveCountMax=3",
"-o",
"StrictHostKeyChecking=accept-new",
"-o",
f"UserKnownHostsFile={knownHostsPath}",
"-i",
privateKeyPath,
f"{sasUser}@{remoteHost}",
]
sshCommand = ["sudo", "-u", sasUser, "-H"] + sshCommand
sshProcess = subprocess.Popen(sshCommand)
sshProcess.wait()
except Exception as exc:
cleanup(f"Error: {exc}")
return 1
finally:
cleanup()
return 0
def remove_tree(path):
if not os.path.exists(path):
return
for rootDir, dirNames, fileNames in os.walk(path, topdown=False):
for fileName in fileNames:
try:
os.unlink(os.path.join(rootDir, fileName))
except OSError:
pass
for dirName in dirNames:
try:
os.rmdir(os.path.join(rootDir, dirName))
except OSError:
pass
try:
os.rmdir(path)
except OSError:
pass
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,185 @@
#!/usr/bin/env bash
# SSH Login Monitor for Fenrir Screen Reader
# Monitors SSH logins and announces them via Fenrir's speech system
# Configuration
fenrirSocket="/tmp/fenrirscreenreader-deamon.sock"
logFile="/var/log/auth.log"
stateFile="/tmp/fenrir-ssh-monitor.state"
checkInterval=2 # seconds between checks
# Voice settings
announceUser=true
announceIp=true
announceHostname=true
announceLogout=false # Announce SSH disconnections (disabled by default - may not work reliably on all systems)
# Function to send message to Fenrir
fenrirSay() {
local message="$1"
# Only announce if Fenrir socket exists (silently skip if not)
if [[ -S "$fenrirSocket" ]]; then
echo "command say ${message}" | socat - UNIX-CLIENT:"${fenrirSocket}" 2>/dev/null
fi
}
# Function to get last processed line number
getLastLine() {
if [[ -f "$stateFile" ]]; then
cat "$stateFile"
else
echo "0"
fi
}
# Function to save last processed line number
saveLastLine() {
echo "$1" > "$stateFile"
}
# Function to parse SSH login and announce
processLogin() {
local logLine="$1"
local user=""
local ip=""
local hostname=""
# Parse different SSH login patterns
# Pattern 1: "Accepted publickey for USER from IP"
# Pattern 2: "Accepted password for USER from IP"
if [[ "$logLine" =~ Accepted\ (publickey|password|keyboard-interactive/pam)\ for\ ([^[:space:]]+)\ from\ ([^[:space:]]+) ]]; then
user="${BASH_REMATCH[2]}"
ip="${BASH_REMATCH[3]}"
# Try to resolve hostname
if command -v host &> /dev/null && [[ "$announceHostname" == "true" ]]; then
hostname="$(host "$ip" 2>/dev/null | grep -oP 'domain name pointer \K[^.]+' | head -1)"
fi
# Build announcement message (concise format)
local message=""
if [[ "$announceUser" == "true" ]]; then
message+="${user} "
fi
message+="S S H login"
if [[ "$announceIp" == "true" ]]; then
message+=" from ${ip}"
fi
if [[ -n "$hostname" ]] && [[ "$announceHostname" == "true" ]]; then
message+=" ${hostname}"
fi
fenrirSay "$message"
return 0
fi
return 1
}
# Function to parse SSH logout and announce
processLogout() {
local logLine="$1"
local user=""
# Parse SSH disconnect patterns
# Pattern: "pam_unix(sshd:session): session closed for user USER"
if [[ "$logLine" =~ session\ closed\ for\ user\ ([^[:space:]]+) ]]; then
user="${BASH_REMATCH[1]}"
if [[ "$announceLogout" == "true" ]]; then
local message=""
if [[ "$announceUser" == "true" ]]; then
message+="${user} "
fi
message+="disconnected from S S H"
fenrirSay "$message"
fi
return 0
fi
return 1
}
# Function to monitor auth.log
monitorAuthLog() {
local lastLine
lastLine=$(getLastLine)
# Get total lines in log
local totalLines
totalLines=$(wc -l < "$logFile" 2>/dev/null || echo "0")
# If log was rotated, reset
if [[ $totalLines -lt $lastLine ]]; then
lastLine=0
fi
# Process new lines
if [[ $totalLines -gt $lastLine ]]; then
local newLines=$((totalLines - lastLine))
# Read only new lines
tail -n "$newLines" "$logFile" 2>/dev/null | while IFS= read -r line; do
if [[ "$line" =~ sshd.*Accepted ]]; then
processLogin "$line"
elif [[ "$line" =~ sshd.*session\ closed ]]; then
processLogout "$line"
fi
done
saveLastLine "$totalLines"
fi
}
# Function to monitor journalctl (alternative for systemd systems)
monitorJournalctl() {
# Follow journalctl for SSH logins and logouts
journalctl -u sshd -u ssh -f -n 0 --no-pager 2>/dev/null | while IFS= read -r line; do
if [[ "$line" =~ Accepted ]]; then
processLogin "$line"
elif [[ "$line" =~ session\ closed ]]; then
processLogout "$line"
fi
done
}
# Check if running as root
if [[ "$(id -u)" -ne 0 ]]; then
echo "Error: This script must be run with sudo privileges to access system logs."
exit 1
fi
# Note: We don't require Fenrir to be running at startup
# The script will silently skip announcements when Fenrir socket doesn't exist
# Determine monitoring method
if command -v journalctl &> /dev/null && systemctl is-active --quiet sshd 2>/dev/null; then
echo "Starting SSH login monitor (using journalctl)..."
fenrirSay "SSH login monitor started."
# Use journalctl for real-time monitoring
trap 'fenrirSay "SSH login monitor stopped."; exit 0' INT TERM
monitorJournalctl
elif [[ -f "$logFile" ]]; then
echo "Starting SSH login monitor (using auth.log)..."
fenrirSay "SSH login monitor started."
# Use auth.log polling
trap 'fenrirSay "SSH login monitor stopped."; rm -f "$stateFile"; exit 0' INT TERM
while true; do
monitorAuthLog
sleep "$checkInterval"
done
else
echo "Error: Cannot find SSH logs. Neither journalctl nor ${logFile} is available."
exit 1
fi
+43 -1
View File
@@ -54,6 +54,9 @@ if [ "$(whoami)" != "root" ]; then
exit 1
fi
# Capture the calling user for chown later
callingUser="${SUDO_USER:-$USER}"
# Check for required tools
for tool in mkarchiso pacman; do
if ! command -v "$tool" &> /dev/null; then
@@ -64,7 +67,6 @@ done
# Get the directory where this script is located
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
pi4_files_dir="$script_dir/../pi4/files"
echo "Building Stormux x86_64 ISO..."
echo "Profile directory: $script_dir"
@@ -105,6 +107,46 @@ fi
# Build the ISO
mkarchiso -v -w "$work_dir" -o "$output_dir" "$script_dir"
# Rename ISO to stormux-x86_64-YYYY-MM-DD.iso
dateStamp="$(date --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y-%m-%d)"
pushd "$output_dir" > /dev/null
shopt -s nullglob
isoFiles=( *.iso )
shopt -u nullglob
if [[ ${#isoFiles[@]} -eq 1 && -f "${isoFiles[0]}" ]]; then
desiredIso="stormux-x86_64-${dateStamp}.iso"
if [[ "${isoFiles[0]}" != "$desiredIso" ]]; then
if [[ -e "$desiredIso" ]]; then
echo "Warning: $desiredIso already exists; skipping rename."
else
mv "${isoFiles[0]}" "$desiredIso"
echo "Renamed ISO to: $desiredIso"
fi
fi
else
echo "Warning: expected one ISO in output, found ${#isoFiles[@]}; skipping rename."
fi
popd > /dev/null
# Generate sha1sum for the ISO
echo
echo "Generating sha1sum..."
pushd "$output_dir" > /dev/null
for isoFile in *.iso; do
if [ -f "$isoFile" ]; then
sha1sum "$isoFile" > "${isoFile}.sha1sum"
echo "Created sha1sum: ${isoFile}.sha1sum"
fi
done
popd > /dev/null
# Change ownership of output directory to calling user
if [ -n "$callingUser" ] && [ "$callingUser" != "root" ]; then
echo "Changing ownership of $output_dir to $callingUser..."
chown -R "$callingUser:$callingUser" "$output_dir"
fi
echo
echo "Build complete!"
echo "ISO file is in: $output_dir"
+5 -1
View File
@@ -2,6 +2,8 @@ alsa-firmware
alsa-utils
amd-ucode
arch-install-scripts
archinstall
autossh
base
base-devel
bash-completion
@@ -19,6 +21,7 @@ cryptsetup
darkhttpd
ddrescue
dhcpcd
dialog
diffutils
dmidecode
dmraid
@@ -42,6 +45,7 @@ gptfdisk
grub
hdparm
hyperv
ii
intel-ucode
irssi
iw
@@ -134,7 +138,7 @@ udftools
usb_modeswitch
usbmuxd
usbutils
vim
vi
virtualbox-guest-utils-nox
vpnc
w3m-git
+2 -1
View File
@@ -25,7 +25,8 @@ Architecture = auto
#IgnorePkg =
#IgnoreGroup =
#NoUpgrade =
# Protect Stormux custom skel files from being overwritten by package updates
NoUpgrade = etc/skel/.bashrc etc/skel/.inputrc etc/skel/.screenrc etc/skel/.vimrc etc/skel/.vim/*
#NoExtract =
# Misc options
+9
View File
@@ -19,13 +19,22 @@ file_permissions=(
["/etc/shadow"]="0:0:400"
["/root"]="0:0:750"
["/root/.automated_script.sh"]="0:0:755"
["/usr/local/bin/sas"]="0:0:755"
["/root/customize_airootfs.sh"]="0:0:755"
["/root/.gnupg"]="0:0:700"
["/root/.config"]="0:0:700"
["/usr/local/bin/choose-mirror"]="0:0:755"
["/usr/local/bin/Installation_guide"]="0:0:755"
["/usr/local/bin/livecd-sound"]="0:0:755"
["/usr/local/bin/configure-stormux"]="0:0:755"
["/usr/local/bin/install-stormux"]="0:0:755"
["/usr/local/bin/stormux-setup.sh"]="0:0:755"
["/usr/local/bin/init-pipewire-sound.sh"]="0:0:755"
["/usr/local/bin/sas.sh"]="0:0:755"
["/usr/local/bin/sas"]="0:0:755"
["/usr/share/fenrirscreenreader/scripts/ssh-login-monitor.sh"]="0:0:755"
["/etc/stormux-assist"]="0:0:755"
["/etc/stormux-assist/client.conf"]="0:0:644"
["/etc/udev/rules.d/99-brltty.rules"]="0:0:644"
["/home/stormux"]="1000:1000:755"
)
+87 -27
View File
@@ -21,10 +21,34 @@
set -e
# Parse command line arguments
bootInstalled=false
while getopts "ih" opt; do
case "$opt" in
i) bootInstalled=true ;;
h)
echo "Usage: $0 [options]"
echo
echo "Options:"
echo " -i Boot from installed system (virtual disk) instead of ISO"
echo " -h Show this help"
echo
echo "Without -i flag, boots from ISO with virtual disk attached for installation."
echo "With -i flag, boots only from virtual disk (installed system)."
exit 0
;;
*)
echo "Use -h for help"
exit 1
;;
esac
done
# Get the directory where this script is located
scriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
outDir="${scriptDir}/out"
logDir="${scriptDir}/logs"
diskFile="${scriptDir}/stormux-test-disk.qcow2"
# Create logs directory if it doesn't exist
mkdir -p "$logDir"
@@ -34,31 +58,54 @@ timestamp=$(date +%Y%m%d_%H%M%S)
qemuLog="${logDir}/qemu-boot_${timestamp}.log"
serialLog="${logDir}/serial-console_${timestamp}.log"
# Find the most recent ISO in the out directory
if [[ ! -d "$outDir" ]]; then
echo "Error: Output directory not found: $outDir"
echo "Please build an ISO first using: sudo ./build.sh"
exit 1
# Create virtual disk if it doesn't exist
if [[ ! -f "$diskFile" ]]; then
echo "Creating virtual disk: $diskFile (10GB)"
qemu-img create -f qcow2 "$diskFile" 10G
echo "Virtual disk created successfully"
echo
fi
# Find the most recent ISO file
isoFile=$(find "$outDir" -name "*.iso" -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-)
# Find ISO file (only needed if not booting installed system)
isoFile=""
if [[ "$bootInstalled" == false ]]; then
# Find the most recent ISO in the out directory
if [[ ! -d "$outDir" ]]; then
echo "Error: Output directory not found: $outDir"
echo "Please build an ISO first using: sudo ./build.sh"
exit 1
fi
if [[ -z "$isoFile" ]]; then
echo "Error: No ISO file found in $outDir"
echo "Please build an ISO first using: sudo ./build.sh"
exit 1
# Find the most recent ISO file
isoFile=$(find "$outDir" -name "*.iso" -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-)
if [[ -z "$isoFile" ]]; then
echo "Error: No ISO file found in $outDir"
echo "Please build an ISO first using: sudo ./build.sh"
exit 1
fi
fi
echo "Found ISO: $isoFile"
echo "Booting Stormux in QEMU with audio support..."
if [[ "$bootInstalled" == true ]]; then
echo "Booting installed system from: $diskFile"
else
echo "Found ISO: $isoFile"
echo "Booting Stormux ISO in QEMU with virtual disk attached..."
echo "Virtual disk: $diskFile"
fi
echo
echo "Logging to:"
echo " QEMU log: $qemuLog"
echo " Serial console: $serialLog"
echo
echo "Note: After boot, press Ctrl+Alt+F2 to switch to tty2"
echo " where the stormux user will be logged in with Fenrir screen reader."
if [[ "$bootInstalled" == false ]]; then
echo "Note: After boot, press Ctrl+Alt+F2 to switch to tty2"
echo " where the stormux user will be logged in with Fenrir screen reader."
echo " Run 'sudo install-stormux' to install to the virtual disk."
echo " After installation, use './qemu-boot.sh -i' to boot the installed system."
else
echo "Note: Booting from installed system on virtual disk."
fi
echo " You can monitor the serial console log in real-time with:"
echo " tail -f $serialLog"
echo
@@ -66,6 +113,30 @@ echo
# Export DISPLAY for console usage
export DISPLAY="${DISPLAY:-:0}"
# Build QEMU command based on boot mode
# Common options for both modes
# shellcheck disable=SC2054
qemuCmd=(
qemu-system-x86_64
-enable-kvm
-m 2048
-drive file="$diskFile",format=qcow2
-audiodev pa,id=snd0
-device intel-hda
-device hda-duplex,audiodev=snd0
-serial file:"$serialLog"
-D "$qemuLog"
-d guest_errors,cpu_reset
-display gtk
)
# Add ISO and boot order if booting from ISO
if [[ "$bootInstalled" == false ]]; then
qemuCmd+=(-cdrom "$isoFile" -boot order=dc)
else
qemuCmd+=(-boot c)
fi
# Boot QEMU with audio support and logging
# -D: QEMU debug log (shows QEMU errors, device issues)
# -d: Debug categories (guest_errors, cpu_reset)
@@ -73,15 +144,4 @@ export DISPLAY="${DISPLAY:-:0}"
# -audiodev: PulseAudio backend for audio
# -device intel-hda: Intel HD Audio controller
# -device hda-duplex: HD Audio codec with input/output, connected to audiodev
exec qemu-system-x86_64 \
-enable-kvm \
-m 2048 \
-cdrom "$isoFile" \
-boot d \
-audiodev pa,id=snd0 \
-device intel-hda \
-device hda-duplex,audiodev=snd0 \
-serial file:"$serialLog" \
-D "$qemuLog" \
-d guest_errors,cpu_reset \
-display gtk
exec "${qemuCmd[@]}"