akshara/akshara
2024-06-05 07:34:10 +00:00

474 lines
16 KiB
Python
Executable file

#!/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 time
import yaml
import shutil
import filecmp
import argparse
import requests
import platform
import fasteners
from threading import Event
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('/')
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)
info('downloading Arch tarball...')
if not os.path.isfile('/.update.tar.zst'):
if exec('wget', '-q', '--show-progress', 'https://geo.mirror.pkgbuild.com/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', 'https://geo.mirror.pkgbuild.com/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', 'sha256sum -c --ignore-missing <(wget -qO- https://geo.mirror.pkgbuild.com/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', 'https://geo.mirror.pkgbuild.com/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', 'sha256sum -c --ignore-missing <(wget -qO- https://geo.mirror.pkgbuild.com/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'
]
keep = [
'/usr/share'
]
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')
with open('/.new_rootfs/etc/pacman.d/mirrorlist', 'w') as pacman_mirrorlist_conf:
pacman_mirrorlist_conf.write('Server = https://cloudflaremirrors.com/archlinux/$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')
counter = 0
while True:
return_val = exec_chroot('pacman', '-Sy', '--ask=4', 'reflector')
counter += 1
if counter > 30:
error('failed to download packages')
exit(50)
if return_val == 0:
break
exec_chroot('reflector', '--latest', '5', '--protocol', 'https', '--sort', 'rate', '--save', '/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', '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:
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:
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)
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('bash', '-c', 'echo "MODULES=()" > /etc/mkinitcpio.conf')
exec_chroot('bash', '-c', 'echo "BINARIES=()" >> /etc/mkinitcpio.conf')
exec_chroot('bash', '-c', 'echo "FILES=()" >> /etc/mkinitcpio.conf')
exec_chroot('bash', '-c', 'echo "HOOKS=(base udev akshara autodetect keyboard keymap modconf block filesystems fsck)" >> /etc/mkinitcpio.conf')
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('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', '/.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')
exec('cp', '-ax', '/.update_rootfs/etc', '/.update_rootfs/usr/etc')
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')