akshara/akshara
2023-05-12 12:42:46 +05:30

414 lines
18 KiB
Python
Executable file

#!/usr/bin/env python3
# Copyright (C) 2023 Rudra Saraswat
#
# This file is part of blend.
#
# blend 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.
#
# blend 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 blend. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import time
import yaml
import shutil
import argparse
import platform
import fasteners
import subprocess
__version = '1.0.0'
with open('/etc/blend_release') as blend_release_file:
blend_release = yaml.load(
blend_release_file, Loader=yaml.FullLoader)
server_url = blend_release['server']
# 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'
# END
def exec(*cmd, **kwargs):
return subprocess.call(cmd, shell=False, **kwargs)
def exec_chroot(*cmd, **kwargs):
return exec('systemd-nspawn', '-D', '/mnt/iso-update/squashfs-root', '--', *cmd, **kwargs)
def info(msg):
print(colors.bold + colors.fg.cyan + '>> i: ' +
colors.reset + colors.bold + msg + colors.reset)
def error(err):
print(colors.bold + colors.fg.red + '>> e: ' +
colors.reset + colors.bold + err + colors.reset)
def get_server_timestamp():
with open('/etc/blend_release') as blend_release_file:
track = yaml.load(blend_release_file, Loader=yaml.FullLoader)['track']
server_version_output = subprocess.run(
['curl', '--silent', '--show-error', f'{server_url}/track/{track}/current'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if server_version_output.stderr.decode().strip() != '':
return 0
elif server_version_output.stdout.decode().strip() != '' and server_version_output.stdout.decode().strip().isnumeric():
return int(server_version_output.stdout.decode().strip())
else:
return 0
def update_system():
os.chdir('/mnt')
exec('rm', '-rf', '/mnt/iso-update/iso')
exec('rm', '-rf', '/mnt/iso-update/squashfs-root')
exec('mkdir', '-p', '/mnt/iso-update')
# Check if update is available
if os.path.isfile('/etc/blend_release'):
with open('/etc/blend_release') as blend_release_file:
blend_release = yaml.load(
blend_release_file, Loader=yaml.FullLoader)
current_timestamp = blend_release['current']
track = blend_release['track']
if get_server_timestamp() > current_timestamp:
# Update is available, let's download the latest ISO
exec('mkdir', '-p', '/mnt/iso-update')
if not os.path.isfile('/mnt/iso-update/update.iso'):
exec('wget', '-O', '/mnt/iso-update/update.iso',
f'{server_url}/track/{track}/download')
exec('rm', '-f', '/mnt/iso-update/update.iso.sha512sum')
exec('wget', '-O', '/mnt/iso-update/update.iso.sha512sum',
f'{server_url}/track/{track}/update-sha512sum')
if exec('bash', '-c', 'cd /mnt/iso-update; sha512sum --check --status /mnt/iso-update/update.iso.sha512sum') != 0:
exec('rm', '-f', '/mnt/iso-update/update.iso.sha512sum',
'/mnt/iso-update/update.iso.sha512sum')
exec('wget', '-O', '/mnt/iso-update/update.iso',
f'{server_url}/track/{track}/download')
exec('wget', '-O', '/mnt/iso-update/update.iso.sha512sum',
f'{server_url}/track/{track}/update-sha512sum')
if exec('sha512sum', '--check', '--status', 'update.iso.sha512sum', cwd='/mnt/iso-update') != 0:
exec('rm', '-f', '/mnt/iso-update/.download_lock')
return
exec('rm', '-f', '/mnt/iso-update/.download_lock')
# Since the ISO has been downloaded, proceed to extracing it
# as well the rootfs it contains (single-core unsquashfs)
exec('rm', '-rf', '/mnt/iso-update/iso',
'/mnt/iso-update/squashfs-root')
exec('7z', '-oiso', 'x', 'update.iso', cwd='/mnt/iso-update')
if exec('unsquashfs', '-p', '1', f'iso/blend/{platform.machine()}/airootfs.sfs', cwd='/mnt/iso-update') != 0:
return
########################
# Configure new rootfs #
########################
# Enable services
exec_chroot('systemctl', 'enable', 'bluetooth')
exec_chroot('systemctl', 'enable', 'cups')
exec_chroot('systemctl', '--global', 'enable', 'blend-files')
# Add akshara hook
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 plymouth autodetect modconf block keyboard keymap consolefont filesystems fsck"\' >> /etc/mkinitcpio.conf')
exec_chroot(
'bash', '-c', 'echo \'COMPRESSION="zstd"\' >> /etc/mkinitcpio.conf')
# Refresh package lists, pacman-key --init
exec_chroot('pacman', '-Rn', '--noconfirm',
'jade-gui', 'blend-inst-git')
exec_chroot('pacman-key', '--init')
exec_chroot('pacman-key', '--populate', 'archlinux', 'blend')
# Disable auto-login for blend user
exec_chroot(
'bash', '-c', 'echo "[Theme]" > /etc/sddm.conf.d/default.conf')
exec_chroot(
'bash', '-c', 'echo "Current=breeze" >> /etc/sddm.conf.d/default.conf')
exec_chroot('rm', '-f', '/etc/gdm/custom.conf')
# Install custom system packages
exec('touch', '/.custom_pkg_list')
exec('cp', '/.custom_pkg_list', '/mnt/iso-update/squashfs-root')
with open('/.custom_pkg_list') as custompkglist_file:
custompkglist = []
while line := custompkglist_file.readline():
if line.strip() != '':
custompkglist.append(line.strip())
exec_chroot('pacman', '-Sy', '--needed', '--noconfirm', *custompkglist)
# Note to self: since the hook only copies new files in /etc, configuring
# Note to self: locales and users isn't required
# Mark as ready for update on boot
exec('touch', '/mnt/iso-update/.ready-for-update')
# Unmount directories if not already unmounted
exec('umount', '-l', '/mnt/iso-update/squashfs-root/dev')
exec('umount', '-l', '/mnt/iso-update/squashfs-root/proc')
def handle_system_packages(operation):
if len(args.pkg) == 0:
error('no packages specified')
exit(1)
for pkg in args.pkg:
if pkg.startswith('linux'):
error('you may not install linux* packages')
exit(1)
if operation == 'set-custom-packages':
info('this operation will replace the current overlay on the next boot')
info('(any custom system packages/drivers installed earlier will be removed)')
print()
info('this command should __only__ be used for the installation of drivers')
info('blendOS is __not__ responsible for any system breakage')
elif operation == 'drop-overlay':
info('any installed packages will be removed')
else:
error('unsupported operation')
print()
time.sleep(2)
exec('mkdir', '-p', '/.blendrw')
while exec('umount', '-l', '/.blendrw/usr', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
pass
while exec('umount', '-l', '/.blendrw/var/lib/pacman', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
pass
while exec('umount', '-l', '/.blendrw', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
pass
for old_overlay in os.listdir('/mnt'):
if old_overlay.startswith('.blend-tmp-overlay-'):
exec('rm', '-rf', f'/mnt/{old_overlay}')
usr_overlay = subprocess.run(
['mktemp', '-d', '/mnt/.blend-tmp-overlay-XXXXXXXXXXXXX'], stdout=subprocess.PIPE).stdout.decode().strip()
exec('chmod', '-R', '755', usr_overlay)
if os.path.isdir('/.blend-overlays/future-usr'):
exec('rm', '-f', '/.blend-overlays/future-usr/.okay')
exec('rm', '-rf', usr_overlay)
exec('mv', '/.blend-overlays/future-usr', usr_overlay)
usr_overlay_workdir = subprocess.run(
['mktemp', '-d', '/mnt/.blend-tmp-overlay-XXXXXXXXXXXXX'], stdout=subprocess.PIPE).stdout.decode().strip()
exec('chmod', '-R', '755', usr_overlay_workdir)
varlibpacman_overlay = subprocess.run(
['mktemp', '-d', '/mnt/.blend-tmp-overlay-XXXXXXXXXXXXX'], stdout=subprocess.PIPE).stdout.decode().strip()
exec('chmod', '-R', '755', varlibpacman_overlay)
if os.path.isdir('/.blend-overlays/future-varlibpacman'):
exec('rm', '-f', '/.blend-overlays/future-varlibpacman/.okay')
exec('rm', '-rf', varlibpacman_overlay)
exec('mv', '/.blend-overlays/future-varlibpacman', varlibpacman_overlay)
varlibpacman_overlay_workdir = subprocess.run(
['mktemp', '-d', '/mnt/.blend-tmp-overlay-XXXXXXXXXXXXX'], stdout=subprocess.PIPE).stdout.decode().strip()
exec('chmod', '-R', '755', varlibpacman_overlay_workdir)
if '' in (usr_overlay, usr_overlay_workdir, varlibpacman_overlay, varlibpacman_overlay_workdir):
for old_overlay in os.listdir('/mnt'):
if old_overlay.startswith('.blend-tmp-overlay-'):
exec('rm', '-rf', f'/mnt/{old_overlay}')
error('error during overlay creation')
exit(1)
exec('mkdir', '-p', '/.blendrw')
exec('mount', subprocess.run(['findmnt', '-n', '-o', 'SOURCE', '/'],
stdout=subprocess.PIPE).stdout.decode().strip(), '/.blendrw')
exec('mount', '-t', 'overlay', 'overlay', '-o',
f'lowerdir=/usr,upperdir={usr_overlay},workdir={usr_overlay_workdir}', '/.blendrw/usr')
exec('mount', '-t', 'overlay', 'overlay', '-o',
f'lowerdir=/var/lib/pacman,upperdir={varlibpacman_overlay},workdir={varlibpacman_overlay_workdir}', '/.blendrw/var/lib/pacman')
if operation == 'set-custom-packages':
operation = ['-Sy', '--needed', '--noconfirm']
elif operation == 'drop-overlay':
exec('rm', '-f', '/.blend-overlays/future-usr/.okay')
exec('rm', '-f', '/.blend-overlays/future-varlibpacman/.okay')
exec('rm', '-f', '/.custom_pkg_list')
exec('touch', '/.custom_pkg_list')
exit()
if exec('systemd-nspawn', '-D', '/.blendrw', '--', 'pacman', *operation, '--', *args.pkg, stdout=sys.stdout, stderr=sys.stderr) != 0:
error('error occurred during installation, abandoning changes')
while exec('umount', '-l', '/.blendrw/usr', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) != 0:
pass
while exec('umount', '-l', '/.blendrw/var/lib/pacman', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) != 0:
pass
while exec('umount', '-l', '/.blendrw', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) != 0:
pass
for old_overlay in os.listdir('/mnt'):
if old_overlay.startswith('.blend-tmp-overlay-'):
exec('rm', '-rf', f'/mnt/{old_overlay}')
elif args.headless:
while exec('umount', '-l', '/.blendrw/usr', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) != 0:
pass
while exec('umount', '-l', '/.blendrw/var/lib/pacman', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) != 0:
pass
while exec('umount', '-l', '/.blendrw', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) != 0:
pass
exec('mv', usr_overlay, '/.blend-overlays/future-usr')
exec('mv', varlibpacman_overlay, '/.blend-overlays/future-varlibpacman')
exec('rm', '-rf', usr_overlay_workdir, varlibpacman_overlay_workdir)
exec('bash', '-c', "printf > /.custom_pkg_list")
for pkg in args.pkg:
exec('bash', '-c', f"echo '{pkg}' >> /.custom_pkg_list")
exec('touch', '/.blend-overlays/future-usr/.okay')
exec('touch', '/.blend-overlays/future-varlibpacman/.okay')
info('reboot to apply changes')
else:
info("you are requested to review the operation's output")
info("press ENTER to proceed with making overlay permanent, or ^C to abort")
info("(aborting will also remove any packages installed previously without rebooting)")
try:
input()
except EOFError:
exit()
while exec('umount', '-l', '/.blendrw/usr', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) != 0:
pass
while exec('umount', '-l', '/.blendrw/var/lib/pacman', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) != 0:
pass
while exec('umount', '-l', '/.blendrw', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) != 0:
pass
exec('mv', usr_overlay, '/.blend-overlays/future-usr')
exec('mv', varlibpacman_overlay, '/.blend-overlays/future-varlibpacman')
exec('rm', '-rf', usr_overlay_workdir, varlibpacman_overlay_workdir)
exec('bash', '-c', "printf > /.custom_pkg_list")
for pkg in args.pkg:
exec('bash', '-c', f"echo '{pkg}' >> /.custom_pkg_list")
exec('touch', '/.blend-overlays/future-usr/.okay')
exec('touch', '/.blend-overlays/future-varlibpacman/.okay')
info('reboot to apply changes')
def daemon():
exec('mkinitcpio', '-P')
exec('grub-mkconfig', '-o', '/boot/grub/grub.cfg')
for dir in os.listdir('/'):
if dir.startswith('.old.'):
shutil.rmtree('/' + dir)
while True:
if not os.path.isfile('/mnt/iso-update/.ready-for-update'):
update_system()
time.sleep(3600)
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}
This CLI is only meant for developers familiar with blendOS and the akshara codebase.
blendOS is not responsible for any system breakage.
{colors.bold}{colors.fg.cyan}available commands{colors.reset}:
{colors.bold}help{colors.reset} Show this help message and exit.
{colors.bold}install{colors.reset} Install system drivers/packages.
{colors.bold}remove{colors.reset} Remove system drivers/packages.
{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',
'set-custom-packages': handle_system_packages,
'drop-overlay': handle_system_packages,
'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 == handle_system_packages:
exec('touch', '/var/lib/.akshara-system-lock')
system_lock = fasteners.InterProcessLock('/var/lib/.akshara-system-lock')
with system_lock:
command(args.command)
else:
command()
except KeyboardInterrupt:
error('aborting')