Compare commits

...

No commits in common. "packaging" and "main" have entirely different histories.

9 changed files with 637 additions and 41 deletions

5
.gitignore vendored
View file

@ -1,5 +0,0 @@
*
!.gitignore
!PKGBUILD
!.SRCINFO

View file

@ -1,36 +0,0 @@
# Maintainer: Rudra Saraswat <rs2009@ubuntu.com>
pkgname='akshara-git'
pkgver=r59.9f2f25b
pkgrel=1
pkgdesc="An update system for operating systems"
arch=('x86_64' 'i686')
url="https://git.askiiart.net/askiiart-blendos/akshara"
license=('GPL3')
makedepends=('git' 'base-devel')
source=('git+https://git.askiiart.net/askiiart-blendos/akshara')
sha256sums=('SKIP')
depends=('bash' 'python' 'python-lockfile' 'python-psutil' 'python-fasteners' 'python-yaml' 'squashfs-tools' 'wget' 'python-requests' 'arch-install-scripts')
provides=("${pkgname%-git}")
conflicts=("${pkgname%-git}")
pkgver() {
cd "${srcdir}/${pkgbase%-git}"
printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
package() {
cd "${srcdir}/${pkgbase%-git}"
install -Dm755 \
"${pkgname%-git}" \
-t "${pkgdir}"/usr/bin/
install -Dm644 "${pkgname%-git}.service" -t \
"${pkgdir}"/usr/lib/systemd/system/
install -Dm644 "${pkgname%-git}-system-update.service" -t \
"${pkgdir}"/usr/lib/systemd/system/
install -Dm644 "${pkgname%-git}.hook" \
"${pkgdir}/usr/lib/initcpio/hooks/${pkgname%-git}"
install -Dm644 "${pkgname%-git}.install" \
"${pkgdir}/usr/lib/initcpio/install/${pkgname%-git}"
}

15
README.md Normal file
View file

@ -0,0 +1,15 @@
# akshara
A simple system builder and immutability layer.
## Development
To test a modified copy of `akshara`, run the following on a working blendOS install:
```sh
umount -l /usr && sudo mv ./akshara /usr/bin/akshara && sudo chmod +x /usr/bin/akshara
```
Replace `./akshara` with wherever your modified copy of `akshara` is.
**ANY CHANGES TO `/usr/bin/akshara` WILL BE REVERTED AFTER EVERY UPDATE!**

508
akshara Executable file
View file

@ -0,0 +1,508 @@
#!/usr/bin/env python3
# Copyright (C) 2023 Rudra Saraswat
#
# This file is part of akshara.
#
# akshara 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 of the License, or
# (at your option) any later version.
#
# akshara 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 akshara. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import yaml
import filecmp
import argparse
import requests
import fasteners
import subprocess
__version = '1.0.0'
# Colors
class colors:
reset = '\033[0m'
bold = '\033[01m'
disable = '\033[02m'
underline = '\033[04m'
reverse = '\033[07m'
strikethrough = '\033[09m'
invisible = '\033[08m'
class fg:
black = '\033[30m'
red = '\033[31m'
green = '\033[32m'
orange = '\033[33m'
blue = '\033[34m'
purple = '\033[35m'
cyan = '\033[36m'
lightgrey = '\033[37m'
darkgrey = '\033[90m'
lightred = '\033[91m'
lightgreen = '\033[92m'
yellow = '\033[93m'
lightblue = '\033[94m'
pink = '\033[95m'
lightcyan = '\033[96m'
class bg:
black = '\033[40m'
red = '\033[41m'
green = '\033[42m'
orange = '\033[43m'
blue = '\033[44m'
purple = '\033[45m'
cyan = '\033[46m'
lightgrey = '\033[47m'
def exec(*cmd, **kwargs):
return subprocess.call(cmd, shell=False, stdout=sys.stdout, stderr=sys.stderr, **kwargs)
def exec_chroot(*cmd, **kwargs):
exec('mount', '--bind', '/.new_rootfs', '/.new_rootfs')
ret = exec('arch-chroot', '/.new_rootfs', *cmd, **kwargs)
exec('umount', '-l', '/.new_rootfs')
return ret
fg = colors.fg()
def info(msg):
print(colors.bold + fg.cyan + '[INFO] ' +
colors.reset + msg + colors.reset)
def warn(warning):
print(colors.bold + fg.yellow + '[WARNING] ' +
colors.reset + warning + colors.reset)
def error(err):
print(colors.bold + fg.red + '[ERROR] ' +
colors.reset + err + colors.reset)
def interpret_track(blend_release):
result = yaml.safe_load(requests.get(blend_release.get('impl') + '/' + blend_release.get('track') + '.yaml', allow_redirects=True).content.decode())
if (type(result.get('impl')) == str and
type(result.get('track')) != 'custom'):
res = interpret_track(result)
for i in res.keys():
if type(res[i]) is list:
if type(result.get(i)) is list:
result[i] = res[i] + result[i]
return result
def update_system():
if os.path.isdir('/.update_rootfs'):
error('update already downloaded, you must reboot first')
sys.exit(75)
os.chdir('/')
if exec('rm', '-rf', '/.new_rootfs', '/.new.etc', '/.new.var.lib') != 0:
exec('umount', '-l', '/.new_rootfs')
exec('rm', '-rf', '/.new_rootfs', '/.new.etc', '/.new.var.lib')
# Check if update is available
if not os.path.isfile('/system.yaml'):
error('system.yaml does not exist in /')
sys.exit(100)
with open('/system.yaml') as blend_release_file:
blend_release = yaml.load(
blend_release_file, Loader=yaml.FullLoader)
# TODO: Add check that all packages actually exist
info('downloading Arch tarball...')
# TODO: currently it errors if it doesn't have arch-repo anyways, so this doesn't need any extra checking, maybe add a check for that later though
# The mirror to use for downloading the bootstrap image
# For example, for the Arch mirror at mirrors.acm.wpi.edu, you'd use https://mirrors.acm.wpi.edu/archlinux
# Not sure why this wouldn't just use `arch-repo` but whatever
bootstrap_mirror = blend_release.get("arch-repo")
if not os.path.isfile('/.update.tar.zst'):
if exec('wget', '-q', '--show-progress', f'{bootstrap_mirror}/iso/latest/archlinux-bootstrap-x86_64.tar.zst', '-O', '/.update.tar.zst') != 0:
warn('failed download')
print()
info('trying download again...')
print()
exec('rm', '-f', '/.update.tar.zst')
if exec('wget', '-q', '--show-progress', f'{bootstrap_mirror}/iso/latest/archlinux-bootstrap-x86_64.tar.zst', '-O', '/.update.tar.zst') != 0:
error('failed download')
print()
error('update failed')
sys.exit(50)
if exec('bash', '-c', f'sha256sum -c --ignore-missing <(wget -qO- {bootstrap_mirror}/iso/latest/sha256sums.txt | sed "s/archlinux-bootstrap-x86_64\\.tar\\.zst/.update.tar.zst/g") 2>/dev/null') != 0:
error('failed checksum verification')
print()
info('trying download again...')
exec('rm', '-f', '/.update.tar.zst')
if exec('wget', '-q', '--show-progress', f'{bootstrap_mirror}/iso/latest/archlinux-bootstrap-x86_64.tar.zst', '-O', '/.update.tar.zst') != 0:
error('failed download')
print()
error('update failed')
sys.exit(50)
return
if exec('bash', '-c', f'sha256sum -c --ignore-missing <(wget -qO- {bootstrap_mirror}/iso/latest/sha256sums.txt | sed "s/archlinux-bootstrap-x86_64\\.tar\\.zst/.update.tar.zst/g") 2>/dev/null') != 0:
error('failed checksum verification')
print()
error('update failed')
sys.exit(25)
return
info('checksum verification was successful')
print()
info('generating new system...')
exec('tar', '--acls', '--xattrs', '-xf', '.update.tar.zst')
exec('mv', 'root.x86_64', '.new_rootfs')
exec('rm', '-f', '/.new_rootfs/pkglist.x86_64.txt')
exec('rm', '-f', '/.new_rootfs/version')
packages = [
'akshara',
'blend'
]
aur_packages = []
services = [
'akshara'
]
user_services = [
'blend-files'
]
persistent_files = []
if (type(blend_release.get('impl')) == str and
type(blend_release.get('track')) != 'custom'):
res = interpret_track(blend_release)
for i in res.keys():
if type(res[i]) is list:
if type(blend_release.get(i)) is list:
blend_release[i] += res[i]
else:
blend_release[i] = res[i]
if type(blend_release.get('packages')) == list:
packages += blend_release.get('packages')
if type(blend_release.get('aur-packages')) == list:
packages += ['fakeroot', 'paru']
aur_packages += blend_release.get('aur-packages')
if type(blend_release.get('services')) == list:
services += blend_release.get('services')
if type(blend_release.get('user-services')) == list:
user_services += blend_release.get('user-services')
if type(blend_release.get('persistent-files')) == list:
persistent_files += blend_release.get('persistent-files')
exec_chroot('rm', '-f', '/.new_rootfs/etc/resolv.conf')
with open('/.new_rootfs/etc/resolv.conf', 'w') as pacman_mirrorlist_conf:
pacman_mirrorlist_conf.write('nameserver 1.1.1.1\n')
with open('/.new_rootfs/etc/pacman.d/mirrorlist', 'w') as pacman_mirrorlist_conf:
if type(blend_release.get('arch-repo')) == str:
pacman_mirrorlist_conf.write(f'Server = {blend_release.get("arch-repo")}/$repo/os/$arch\n')
else:
pacman_mirrorlist_conf.write('Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch\n')
exec_chroot('mkdir', '-p', '/var/cache/pacman/pkg')
exec_chroot('rm', '-rf', '/var/cache/pacman/pkg')
exec('cp', '-r', '/var/cache/pacman/pkg', '/.new_rootfs/var/cache/pacman')
# update packages
exec_chroot('pacman-key', '--init')
exec_chroot('pacman-key', '--populate')
exec_chroot('sh', '-c', 'mkdir /tmp/rate-mirrors/; cd /tmp/rate-mirrors/; curl -LO $(curl -s https://api.github.com/repos/westandskif/rate-mirrors/releases/latest | grep "browser_download_url.*rate-mirrors-v.*-x86_64-unknown-linux-musl.tar.gz" | cut -d : -f 2,3 | tr -d \\")')
exec_chroot('bash', '-c', 'cd /tmp/rate-mirrors/; tar -xzf rate-mirrors*; cd $(find /tmp/rate-mirrors/ -mindepth 1 -maxdepth 1 -type d); ./rate_mirrors --disable-comments-in-file --entry-country=US --protocol=https arch --max-delay 7200 > /etc/pacman.d/mirrorlist')
#exec_chroot('sed', 's/#//g', '-i', '/etc/pacman.d/mirrorlist')
#exec_chroot('bash', '-c', 'grep "^Server =" /etc/pacman.d/mirrorlist > /etc/pacman.d/mirrorlist.tmp; mv /etc/pacman.d/mirrorlist.tmp /etc/pacman.d/mirrorlist')
with open('/.new_rootfs/etc/pacman.conf', 'r') as original: data = original.read()
with open('/.new_rootfs/etc/pacman.conf', 'w') as modified: modified.write(data.replace("[options]", "[options]\nParallelDownloads = 32\n"))
with open('/.new_rootfs/etc/pacman.conf', 'w') as modified: modified.write(data.replace("#[multilib]\n#Include = /etc/pacman.d/mirrorlist", "[multilib]\nInclude = /etc/pacman.d/mirrorlist"))
with open('/.new_rootfs/etc/pacman.conf', 'a') as pacman_conf:
pacman_conf.write(f'''
[breakfast]
SigLevel = Never
Server = {blend_release['repo']}
''')
if type(blend_release.get('package-repos')) == list:
for package_repo in blend_release.get('package-repos'):
if (type(package_repo.get('name')) == str and
type(package_repo.get('repo-url')) == str):
pacman_conf.write(f'''
[{package_repo["name"]}]
SigLevel = Never
Server = {package_repo["repo-url"]}
''')
counter = 0
while True:
return_val = exec_chroot('pacman', '-Syu', '--noconfirm')
counter += 1
if counter > 30:
error('failed to download packages')
exit(50)
if return_val == 0:
break
exec('cp', '/etc/mkinitcpio.conf', '/.new_rootfs/etc/mkinitcpio.conf')
counter = 0
while True:
print('running packages again')
return_val = exec_chroot('pacman', '-S', '--ask=4', *packages)
counter += 1
if counter > 30:
error('failed to download packages')
exit(50)
if return_val == 0:
break
counter = 0
if aur_packages != []:
while True:
print('running aur_packages again')
exec_chroot('useradd', '-m', '-G', 'wheel', '-s', '/bin/bash', 'aur')
exec_chroot('bash', '-c', 'echo "aur ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/aur')
return_val = exec_chroot(
'runuser', '-u', 'aur', '--', 'paru', '-Sy', '--noconfirm', '--needed',
'--noprogressbar', '--skipreview', '--removemake', '--cleanafter', '--ask=4',
*aur_packages)
exec_chroot('userdel', '-r', 'aur')
exec_chroot('rm', '-f', '/etc/sudoers.d/aur')
counter += 1
if counter > 30:
error('failed to download AUR packages')
exit(50)
if return_val == 0:
break
for service in services:
if type(service) is str:
exec_chroot('systemctl', 'enable', service)
for user_service in user_services:
if type(user_service) is str:
exec_chroot('systemctl', 'enable', '--global', user_service)
for persistent_file in persistent_files:
if type(persistent_file) is str:
if os.path.exists(persistent_file):
exec('mkdir', '-p', '/.new_rootfs/'+ os.path.dirname(persistent_file))
exec('cp', persistent_file, '/.new_rootfs/'+ persistent_file)
if type(blend_release.get('commands')) == list:
for command in blend_release.get('commands'):
if type(command) == str:
exec_chroot('bash', '-c', command)
elif type(command) == list:
exec_chroot(*command)
kernel_exists = False
for f in os.listdir('/.new_rootfs/boot'):
if f.startswith('vmlinuz'):
kernel_exists = True
break
if not kernel_exists:
error('no Linux kernel found in new system')
error('cancelling update so as not to render the system unbootable')
sys.exit(10)
exec_chroot('pacman', '-S', '--noconfirm', 'shadow')
exec_chroot('mkinitcpio', '-P')
exec('cp', '-ax', '/etc/locale.gen', '/.new_rootfs/etc/locale.gen')
exec_chroot('locale-gen')
exec_chroot('rm', '-f', '/.new_rootfs/etc/resolv.conf')
exec('cp', '-ax', '/.new_rootfs/etc', '/.new.etc')
if os.environ.get('AKSHARA_INSTALL') == '1':
exec('rm', '-rf', '/usr/etc')
exec('cp', '-ax', '/.new_rootfs/etc', '/usr/etc')
etc_diff = filecmp.dircmp('/etc/', '/usr/etc/')
def get_diff_etc_files(dcmp):
dir_name = dcmp.left.replace('/etc/', '/.new.etc/', 1)
for name in dcmp.left_only:
exec('mkdir', '-p', dir_name)
exec('cp', '-ax', os.path.join(dcmp.left, name), dir_name)
for name in dcmp.diff_files:
exec('cp', '-ax', os.path.join(dcmp.left, name), dir_name)
for sub_dcmp in dcmp.subdirs.values():
get_diff_etc_files(sub_dcmp)
get_diff_etc_files(etc_diff)
exec('cp', '-ax', '/var/lib', '/.new.var.lib')
var_lib_diff = filecmp.dircmp('/.new_rootfs/var/lib/', '/.new.var.lib/')
dir_name = '/.new.var.lib/'
for name in var_lib_diff.left_only:
if os.path.isdir(os.path.join(var_lib_diff.left, name)):
exec('cp', '-ax', os.path.join(var_lib_diff.left, name), dir_name)
exec('cp', '/etc/passwd', '/.new_rootfs/etc')
exec('cp', '/etc/group', '/.new_rootfs/etc')
exec('cp', '/etc/shadow', '/.new_rootfs/etc')
exec('cp', '/etc/gshadow', '/.new_rootfs/etc')
exec_chroot('systemd-sysusers')
exec('cp', '/.new_rootfs/etc/passwd', '/.new.etc')
exec('cp', '/.new_rootfs/etc/group', '/.new.etc')
exec('cp', '/.new_rootfs/etc/shadow', '/.new.etc')
exec('cp', '/.new_rootfs/etc/gshadow', '/.new.etc')
exec('cp', '/.new_rootfs/etc/pacman.conf', '/.new.etc')
exec('rm', '-rf', '/.new.etc/systemd/system')
exec('cp', '-ax', '/.new_rootfs/etc/systemd/system', '/.new.etc/systemd')
exec('rm', '-rf', '/.new.var.lib/pacman')
exec('cp', '-ax', '/.new_rootfs/var/lib/pacman', '/.new.var.lib/pacman')
exec('mv', '.new_rootfs', '.update_rootfs')
# don't move /etc/shells to /usr/etc
exec('bash', '-c', 'shopt -s extglob; cd /.update_rootfs/etc; cp -ax !(shells) /.update_rootfs/usr/etc; cd -')
new_boot_files = []
for f in os.listdir('/.update_rootfs/boot'):
if not os.path.isdir(f'/.update_rootfs/boot/{f}'):
exec('mv', f'/.update_rootfs/boot/{f}', '/boot')
new_boot_files.append(f)
for f in os.listdir('/boot'):
if not os.path.isdir(f'/boot/{f}'):
if f not in new_boot_files:
exec('rm', '-f', f'/boot/{f}')
exec('grub-mkconfig', '-o', '/boot/grub/grub.cfg')
exec('touch', '/.update')
info('downloaded update and generated new rootfs')
info('you may reboot now')
def daemon():
for dir in os.listdir('/'):
if dir.startswith('.old.'):
exec('rm', '-rf', '/' + dir)
description = f'''
{colors.bold}{colors.fg.cyan}usage:{colors.reset}
{os.path.basename(sys.argv[0])} [command] [options] [arguments]
{colors.bold}{colors.fg.cyan}version:{colors.reset} {__version}{colors.bold}
{colors.bold}{colors.fg.cyan}available commands{colors.reset}:
{colors.bold}help{colors.reset} Show this help message and exit.
{colors.bold}update{colors.reset} Update your blendOS system.
{colors.bold}version{colors.reset} Show version information and exit.
{colors.bold}{colors.fg.cyan}options for commands{colors.reset}:
{colors.bold}-v, --version{colors.reset} show version information and exit
'''
epilog = f'''
{colors.bold}Made with {colors.fg.red}\u2764{colors.reset}{colors.bold} by Rudra Saraswat.{colors.reset}
'''
parser = argparse.ArgumentParser(description=description, usage=argparse.SUPPRESS,
epilog=epilog, formatter_class=argparse.RawTextHelpFormatter)
command_map = {'help': 'help',
'version': 'version',
'update': update_system,
'daemon': daemon}
parser.add_argument('command', choices=command_map.keys(),
help=argparse.SUPPRESS)
parser.add_argument('pkg', action='store', type=str,
nargs='*', help=argparse.SUPPRESS)
parser.add_argument('--headless',
action='store_true', help=argparse.SUPPRESS)
parser.add_argument('-v', '--version', action='version',
version=f'%(prog)s {__version}', help=argparse.SUPPRESS)
if len(sys.argv) == 1:
parser.print_help()
exit()
if os.geteuid() != 0 and not sys.argv[1] in ('help', 'version', '-v', '--version'):
error('requires root')
exit(1)
args = parser.parse_intermixed_args()
command = command_map[args.command]
try:
if command == 'help':
parser.print_help()
elif command == 'version':
parser.parse_args(['--version'])
elif command == update_system:
exec('touch', '/var/lib/.akshara-system-lock')
system_lock = fasteners.InterProcessLock('/var/lib/.akshara-system-lock')
info('attempting to acquire system lock')
with system_lock:
command()
else:
command()
except KeyboardInterrupt:
error('aborting')
# remove update and akshara stuff if the program is exited
exec('rm', '-rf', '/.new_rootfs/')
exec('rm', '-rf', '/.update_rootfs')
# it's basically impossible to ^C before akshara has exited but after it's created this file
# and i don't *think* that "update" would be destructive at all (unless it failed to run and would't boot), but i don't quite understand it soooo just to be on the safe side
exec('rm', '-f', '/.update')
# TODO: add similar handling for errors, and an option to not delete stuff on error/early exit

View file

@ -0,0 +1,10 @@
[Unit]
Description=Update system
[Service]
Type=simple
ExecStart=/usr/bin/akshara update
User=root
[Install]
WantedBy=multi-user.target

55
akshara.hook Executable file
View file

@ -0,0 +1,55 @@
#!/bin/bash
run_latehook() {
echo
# Remove /new_root/.successful-update if exists
rm -f /new_root/.successful-update /new_root/.update
# Detect if update downloaded.
if [[ -d /new_root/.update_rootfs ]]; then
# Available, rename old /usr and move new /usr to /.
if [[ -d /new_root/.update_rootfs/usr ]]; then
mv /new_root/usr /new_root/.old.usr
mv /new_root/.update_rootfs/usr /new_root/usr
fi
# Same for /etc.
if [[ -d /new_root/.update_rootfs/etc ]]; then
mv /new_root/.update_rootfs/etc /new_root/usr/etc
fi
if [[ -d /new_root/.new.etc ]]; then
mv /new_root/etc /new_root/.old.etc
mv /new_root/.new.etc /new_root/etc
fi
# Same for /opt
if [[ -d /new_root/.update_rootfs/opt ]]; then
mv /new_root/opt /new_root/.old.opt
mv /new_root/.update_rootfs/opt /new_root/opt
fi
# Same for /var.
if [[ -d /new_root/.new.var.lib ]]; then
mv /new_root/var/lib /new_root/.old.var.lib
mv /new_root/.new.var.lib /new_root/var/lib
fi
if [[ -d /new_root/.update_rootfs/var/cache/pacman ]]; then
mv /new_root/var/cache/pacman /new_root/.old.var.cache.pacman
mv /new_root/.update_rootfs/var/cache/pacman /new_root/var/cache/pacman
fi
mv /new_root/.update_rootfs /new_root/.old.update_rootfs
touch /new_root/.successful-update
fi
for i in usr varlibpacman usrlocal; do
rm -rf /new_root/.blend-overlays/$i.workdir
mkdir -p /new_root/.blend-overlays/$i
mkdir -p /new_root/.blend-overlays/$i.workdir
done
mount -t overlay overlay -o index=off -o metacopy=off -o ro,lowerdir=/new_root/usr,upperdir=/new_root/.blend-overlays/usr,workdir=/new_root/.blend-overlays/usr.workdir /new_root/usr
mount -t overlay overlay -o index=off -o metacopy=off -o ro,lowerdir=/new_root/var/lib/pacman,upperdir=/new_root/.blend-overlays/varlibpacman,workdir=/new_root/.blend-overlays/varlibpacman.workdir /new_root/var/lib/pacman
mount -t overlay overlay -o rw,lowerdir=/new_root/usr/local,upperdir=/new_root/.blend-overlays/usrlocal,workdir=/new_root/.blend-overlays/usrlocal.workdir /new_root/usr/local
}

23
akshara.install Normal file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-3.0
build() {
add_module overlay
add_binary bash
add_binary xargs
add_binary find
add_binary grep
add_binary findmnt
add_binary uname
add_binary chroot
add_runscript
}
help() {
cat <<HELPEOF
This hook handles system updates and overlays. No
configuration is needed.
HELPEOF
}
# vim: set ft=sh ts=4 sw=4 et:

10
akshara.service Normal file
View file

@ -0,0 +1,10 @@
[Unit]
Description=Handle system operations
[Service]
Type=simple
ExecStart=/usr/bin/akshara daemon
User=root
[Install]
WantedBy=multi-user.target

16
system.yaml.sample Normal file
View file

@ -0,0 +1,16 @@
repo: 'https://pkg-repo.blendos.co/'
impl: 'https://github.com/blend-os/tracks/raw/main'
track: 'gnome'
packages:
- 'micro'
- 'caddy'
services:
- 'caddy'
package-repos:
- name: 'chaotic-aur'
repo-url: 'https://cdn-mirror.chaotic.cx/$repo/$arch'