commit 8085c747cbd4223eab82092d139b8a0807834948 Author: Rudra Saraswat Date: Wed Apr 1 21:26:27 2026 +0100 feat: initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/usr/bin/akshara b/usr/bin/akshara new file mode 120000 index 0000000..ffe9864 --- /dev/null +++ b/usr/bin/akshara @@ -0,0 +1 @@ +../lib/akshara/akshara \ No newline at end of file diff --git a/usr/lib/akshara/__init__.py b/usr/lib/akshara/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/usr/lib/akshara/akshara b/usr/lib/akshara/akshara new file mode 100755 index 0000000..22bbc57 --- /dev/null +++ b/usr/lib/akshara/akshara @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 + +import os +import subprocess +import sys +import time +from pathlib import Path + +import click +import fasteners +import yaml +from utils.gen_rootfs import gen_rootfs +from utils.helpers import ( + get_system_config, + is_already_latest, + notify_prompt, + output, + resolve_config, +) +from utils.update import update + + +@click.group() +def cli(): + """Handles system operations.""" + + +@cli.command("gen-rootfs") +@click.argument("config_file", type=click.File("r")) +@click.argument( + "rootfs_path", type=click.Path(exists=True, file_okay=False, dir_okay=True) +) +def cmd_gen_rootfs(config_file, rootfs_path): + """Generates a rootfs using a provided configuration file.""" + gen_rootfs(resolve_config(yaml.safe_load(config_file.read())), rootfs_path) + + +@cli.command("update-check", hidden=True) +def update_check_daemon(): + if os.environ.get("USER") == "gdm-greeter": + exit() + + check_interval = 3600 + + if isinstance(get_system_config().get("auto-update"), dict): + if not get_system_config()["auto-update"]["enabled"]: + exit() + if isinstance(get_system_config()["auto-update"].get("interval"), int): + check_interval = get_system_config()["auto-update"]["interval"] + + while True: + try: + if not os.path.isdir("/.update_rootfs"): + if not is_already_latest(): + if notify_prompt( + title="Update available", + body="A system update is available", + actions={"update": "Update in the background"}, + ): + if ( + subprocess.run(["pkexec", "akshara", "update"]).returncode + == 0 + ): + if ( + notify_prompt( + title="System updated", + body="Reboot to apply update?", + actions={"reboot": "Reboot now", "later": "Later"}, + ) + == "reboot" + ): + subprocess.run(["reboot"]) + except Exception: + pass + + time.sleep(check_interval) + + +@cli.command("update") +@click.option("-f", "--force", is_flag=True) +def update_cmd(force): + """ + Update your system. + """ + + if os.geteuid() != 0: + output.error("must be run as root") + exit(1) + + Path("/var/lib/akshara").mkdir(parents=True, exist_ok=True) + system_lock = fasteners.InterProcessLock("/var/lib/akshara/.system-lock") + output.info("attempting to acquire system lock") + output.info( + "if stuck at this line for long, an update may be progressing in the background" + ) + + with system_lock: + print() + output.info("checking if already up-to-date...") + + if (not os.path.isdir("/.update_rootfs")) or force: + if is_already_latest() and not force: + output.info("your system is already up-to-date") + sys.exit(0) + else: + output.error( + "an update has already been downloaded and is waiting to be applied" + ) + output.error("you must reboot before running this command") + sys.exit(1) + + update() + output.info("update complete; you may now reboot.") + + +cli() diff --git a/usr/lib/akshara/classes/__init__.py b/usr/lib/akshara/classes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/usr/lib/akshara/classes/rootfs.py b/usr/lib/akshara/classes/rootfs.py new file mode 100644 index 0000000..2774b55 --- /dev/null +++ b/usr/lib/akshara/classes/rootfs.py @@ -0,0 +1,119 @@ +import os +import subprocess +from pathlib import Path + + +class RootFS: + """Handles root filesystem operations. + + Attributes: + rootfs_path: A string containing the path to the root filesystem + """ + + def __init__(self, rootfs_path: str, distro_config: dict) -> None: + """Initialises an instance based on a rootfs path and distro configuration. + + Args: + rootfs: Path to root filesystem. + distro_config: Dictionary containing distro configuration. + """ + self.rootfs_path = rootfs_path + self.distro_config = distro_config + + def exists(self, path): + """Checks if path exists within rootfs. + + Args: + path: Path to check for. + """ + return os.path.exists(os.path.join(self.rootfs_path, path)) + + def init(self) -> subprocess.CompletedProcess: + """Initialise rootfs.""" + + # Create blendOS cache directory + Path("/var/cache/blendOS").mkdir(parents=True, exist_ok=True) + Path(os.path.join(self.rootfs_path, "var/cache/blendOS")).mkdir( + parents=True, exist_ok=True + ) + # subprocess.run( + # ["rm", "-rf", "--", os.path.join(self.rootfs_path, "var/cache/blendOS")] + # ) + # subprocess.run( + # [ + # "cp", + # "-a", + # "--", + # "/var/cache/blendOS", + # os.path.join(self.rootfs_path, "var/cache/blendOS"), + # ] + # ) + + subprocess.run( + [ + "mount", + "--bind", + "/var/cache/blendOS", + os.path.join(self.rootfs_path, "var/cache/blendOS"), + ] + ) + + completedProcess = subprocess.run( + ["bash", "-s"], + text=True, + input=self.distro_config["before-stages"], + cwd=self.rootfs_path, + ) + + subprocess.run(["umount", os.path.join(self.rootfs_path, "var/cache/blendOS")]) + + return completedProcess + + def exec(self, cmd, **kwargs) -> subprocess.CompletedProcess: + """Runs command within rootfs. + + Args: + cmd: List comprising command and arguments. + **kwargs: Keyword arguments list for subprocess.run(). + """ + return subprocess.run( + [ + "systemd-nspawn", + "--quiet", + "--pipe", + "--bind=/var/cache/blendOS", + "-D", + self.rootfs_path, + ] + + list(cmd), + **kwargs, + ) + + # def copy_kernels_to_boot(self) -> None: + # """Copies any found kernels to /boot within rootfs.""" + # kernels = [ + # kernel + # for kernel in os.listdir(f"{self.rootfs_path}/usr/lib/modules") + # if self.exists(f"/usr/lib/modules/{kernel}/vmlinuz") + # ] + + # if len(kernels) == 0: + # return + + # for boot_file in os.listdir(f"{self.rootfs_path}/boot"): + # if not os.path.isdir(boot_file): + # self.exec(["rm", "-f", f"/boot/{boot_file}"]) + + # for kernel in kernels: + # self.exec( + # ["cp", f"/usr/lib/modules/{kernel}/vmlinuz", f"/boot/vmlinuz-{kernel}"], + # stdout=subprocess.DEVNULL, + # stderr=subprocess.DEVNULL, + # ) + + # def gen_initramfs(self) -> None: + # """Generates initramfs within rootfs.""" + # self.exec(["dracut", "--force", "--regenerate-all"]) + + def __repr__(self) -> str: + return self.rootfs_path diff --git a/usr/lib/akshara/utils/__init__.py b/usr/lib/akshara/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/usr/lib/akshara/utils/gen_rootfs.py b/usr/lib/akshara/utils/gen_rootfs.py new file mode 100644 index 0000000..120bf3f --- /dev/null +++ b/usr/lib/akshara/utils/gen_rootfs.py @@ -0,0 +1,43 @@ +import json +import os + +from classes.rootfs import RootFS + +from . import output + + +def run_script_rootfs(rootfs: RootFS, input: str, args: list): + """Run a script within the rootfs.""" + + rootfs.exec(["bash", "-s", *args], text=True, input=input) + + +def gen_rootfs(system_config: dict, rootfs_path: str) -> RootFS: + """Generates a rootfs for a given system configuration.""" + + rootfs = RootFS(rootfs_path, system_config["distro-config"]) + rootfs.init() + + modules = {} + + if isinstance(system_config.get("modules"), list): + for module in system_config["modules"]: + modules[module["name"]] = module["run"] + + if isinstance(system_config.get("stages"), list): + for stage in system_config["stages"]: + if stage.get("module") not in modules.keys(): + output.error(f"{stage.get('module')} not found within module list.") + exit(1) + + inputs = stage["inputs"] if isinstance(stage.get("inputs"), list) else [] + run_script_rootfs(rootfs, modules[stage["module"]], inputs) + + if isinstance(system_config["distro-config"].get("after-stages"), str): + run_script_rootfs(rootfs, system_config["distro-config"]["after-stages"], []) + + with open(os.path.join(rootfs_path, "usr/system.json"), "w") as system_json_file: + json.dump(system_config, system_json_file, ensure_ascii=False) + pass + + return rootfs diff --git a/usr/lib/akshara/utils/helpers.py b/usr/lib/akshara/utils/helpers.py new file mode 100644 index 0000000..85e8700 --- /dev/null +++ b/usr/lib/akshara/utils/helpers.py @@ -0,0 +1,129 @@ +import json +import os +import subprocess + +import requests +import yaml + +from . import output + + +def resolve_config(system_config: dict) -> dict: + if system_config["track"] == "base": + try: + return { + "track": "base", + "modules": system_config["modules"] + if isinstance(system_config.get("modules"), list) + else [], + "stages": system_config["stages"] + if isinstance(system_config.get("stages"), list) + else [], + "distro-config": system_config["distro-config"], + "auto-update": system_config["auto-update"] + if isinstance(system_config.get("auto-update"), dict) + else {"enabled": False}, + } + except IndexError: + output.error("base track must include distro-config") + exit(1) + else: + base_track_src = system_config["track"] + if os.path.exists(base_track_src): + base_config = resolve_config(yaml.safe_load(base_track_src)) + elif base_track_src.startswith("http:") or base_track_src.startswith("https:"): + base_config = resolve_config( + yaml.safe_load( + requests.get(base_track_src, allow_redirects=True).content.decode() + ) + ) + pass + else: + output.error(f"could not resolve parent track - {system_config['track']}") + exit(1) + + base_config["modules"] += ( + system_config["modules"] + if isinstance(system_config.get("modules"), list) + else [] + ) + + base_config["stages"] += ( + system_config["stages"] + if isinstance(system_config.get("stages"), list) + else [] + ) + + base_config["auto-update"] = ( + system_config["auto-update"] + if isinstance(system_config.get("auto-update"), dict) + else base_config["auto-update"] + ) + + return base_config + + +def get_system_config(): + if os.path.exists("/system.yaml"): + with open("/system.yaml") as system_yaml_file: + system_config = yaml.safe_load(system_yaml_file.read()) + elif os.path.exists("/system.yml"): + with open("/system.yml") as system_yml_file: + system_config = yaml.safe_load(system_yml_file.read()) + elif os.path.exists("/system.json"): + with open("/system.json") as system_json_file: + system_config = yaml.safe_load(system_json_file.read()) + else: + output.error("no system config file found") + exit(1) + + return resolve_config(system_config) + + +def is_already_latest() -> bool: + """Checks if system is already up-to-date. + + Returns: + True if up-to-date, else False. + """ + + if os.path.isfile("/usr/system.json"): + with open("/usr/system.json") as system_json_file: + contents = system_json_file.read().strip() + if ( + json.dumps(get_system_config(), ensure_ascii=False).strip() + == contents.strip() + ): + return True + else: + return False + else: + return False + + +def notify_prompt(title: str, body: str, actions: dict) -> str: + """Display a notification prompting the user. + + Args: + title: Title of the notification. + body: Body of the notification. + actions: Dict containing action key-value pairs. + + Returns: + String containing selected action. + """ + return ( + subprocess.run( + [ + "notify-send", + "--app-name=System", + "--urgency=critical", + title, + body, + *[f"--action={action}={actions[action]}" for action in actions.keys()], + ], + stdout=subprocess.PIPE, + ) + .stdout.decode() + .strip() + ) diff --git a/usr/lib/akshara/utils/output.py b/usr/lib/akshara/utils/output.py new file mode 100644 index 0000000..b1f2445 --- /dev/null +++ b/usr/lib/akshara/utils/output.py @@ -0,0 +1,27 @@ +import sys + + +def info(msg: str) -> None: + """Print an informative message to stdout. + + Args: + msg: String containing the message. + """ + print(f"I: {msg}") + + +def warn(msg: str) -> None: + """Print a warning message to stderr. + + Args: + msg: String containing the message.""" + print(f"W: {msg}", file=sys.stderr) + + +def error(msg: str) -> None: + """Print an error message to stderr. + + Args: + msg: String containing the message. + """ + print(f"E: {msg}", file=sys.stderr) diff --git a/usr/lib/akshara/utils/update.py b/usr/lib/akshara/utils/update.py new file mode 100644 index 0000000..f86bbdc --- /dev/null +++ b/usr/lib/akshara/utils/update.py @@ -0,0 +1,224 @@ +import filecmp +import os +import subprocess +import sys +from pathlib import Path + +from classes.rootfs import RootFS +from utils import output, users + +from . import helpers +from .gen_rootfs import gen_rootfs + + +def update_cleanup() -> None: + """Clean-up from previous rebase/update.""" + subprocess.run( + ["umount", "-l", "/var/cache/akshara/rootfs/var/cache/blendOS"], + stdout=subprocess.DEVNULL, + ) + + subprocess.run( + [ + "rm", + "-rf", + "/var/cache/akshara/rootfs", + "/.update_rootfs", + ] + ) + + +def merge_etc(new_rootfs: RootFS, overrides_keep_new: dict) -> None: + """Merges /etc trees. + + Args: + new_rootfs: New RootFS instance. + overrides_keep_new: Dictionary comprising overrides and whether to keep new. + """ + subprocess.run(["cp", "-ax", f"{new_rootfs}/etc", f"{new_rootfs}/usr/etc"]) + + if not os.path.isdir("/usr/etc"): + subprocess.run(["rm", "-rf", "/usr/etc"]) + subprocess.run(["cp", "-ax", "/etc", "/usr/etc"]) + + etc_diff = filecmp.dircmp("/etc/", "/usr/etc/") + + def handle_diff_etc_files(dcmp): + dir_name = dcmp.left.replace("/etc/", f"{new_rootfs}/etc/", 1) + subprocess.run(["mkdir", "-p", dir_name]) + for name in dcmp.left_only: + subprocess.run(["cp", "-ax", "--", os.path.join(dcmp.left, name), dir_name]) + for name in dcmp.diff_files: + subprocess.run(["cp", "-ax", "--", os.path.join(dcmp.left, name), dir_name]) + for sub_dcmp in dcmp.subdirs.values(): + handle_diff_etc_files(sub_dcmp) + + handle_diff_etc_files(etc_diff) + + for override, keep_new in overrides_keep_new.items(): + if ( + override.startswith("/etc/") + and os.path.exists(override) + and new_rootfs.exists(override) + ): + subprocess.run(["rm", "-rf", f"{new_rootfs}/{override}"]) + if keep_new: + subprocess.run( + [ + "cp", + "-ax", + f"{new_rootfs}/usr/{override}", + f"{new_rootfs}/{override}", + ] + ) + else: + subprocess.run( + [ + "cp", + "-ax", + override, + f"{new_rootfs}/{override}", + ] + ) + + +def merge_var(new_rootfs: RootFS, overrides_keep_new: dict) -> None: + """Merges /var trees. + + Args: + new_rootfs: Path to rootfs. + overrides_keep_new: Dictionary comprising overrides and whether to keep new. + """ + subprocess.run(["mv", f"{new_rootfs}/var", f"{new_rootfs}/usr/var"]) + subprocess.run(["cp", "-ax", "/var", f"{new_rootfs}/var"]) + + var_lib_diff = filecmp.dircmp( + f"{new_rootfs}/usr/var/lib/", f"{new_rootfs}/var/lib/" + ) + + dir_name = f"{new_rootfs}/var/lib/" + for name in var_lib_diff.left_only: + if os.path.isdir(os.path.join(var_lib_diff.left, name)): + subprocess.run( + ["cp", "-ax", os.path.join(var_lib_diff.left, name), dir_name] + ) + + for override, keep_new in overrides_keep_new.items(): + if ( + override.startswith("/var/") + and os.path.exists(override) + and new_rootfs.exists(override) + ): + subprocess.run(["rm", "-rf", f"{new_rootfs}/{override}"]) + if keep_new: + subprocess.run( + [ + "cp", + "-ax", + f"{new_rootfs}/usr/{override}", + f"{new_rootfs}/{override}", + ] + ) + else: + subprocess.run( + [ + "cp", + "-ax", + override, + f"{new_rootfs}/{override}", + ] + ) + + +def replace_boot_files() -> None: + """Replace files in /boot with those from new rootfs.""" + new_boot_files = [] + + for f in os.listdir("/.update_rootfs/boot"): + if not os.path.isdir(f"/.update_rootfs/boot/{f}"): + subprocess.run(["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: + subprocess.run(["rm", "-f", f"/boot/{f}"]) + + subprocess.run(["grub-mkconfig", "-o", "/boot/grub/grub.cfg"]) + + +def update() -> None: + """Update system. + + Args: + image_name: Name of image to rebase to. + """ + + update_cleanup() + + system_config = helpers.get_system_config() + + output.info("generating new rootfs") + + subprocess.run(["rm", "-rf", "/var/cache/akshara"]) + Path("/var/cache/akshara/rootfs").mkdir(parents=True, exist_ok=True) + + new_rootfs = gen_rootfs(system_config, "/var/cache/akshara/rootfs") + + overrides_keep_new = ( + { + override["path"]: override["keep"] == "new" + for override in system_config["override"] + } + if isinstance(system_config.get("override"), list) + else {} + ) + + merge_etc(new_rootfs, overrides_keep_new) + + try: + # Store new_passwd_entries for users.merge_group() call + new_passwd_entries = users.merge_passwd(new_rootfs) + except Exception: + output.error("malformed /etc/passwd") + sys.exit(1) + + try: + users.merge_shadow(new_rootfs) + except Exception: + output.error("malformed /etc/shadow") + sys.exit(1) + + try: + users.merge_group(new_rootfs, new_passwd_entries) + except Exception: + output.error("malformed /etc/group") + sys.exit(1) + + try: + users.merge_gshadow(new_rootfs, new_passwd_entries) + except Exception: + output.error("malformed /etc/shadow") + sys.exit(1) + + merge_var(new_rootfs, overrides_keep_new) + + if ( + len( + [ + kernel + for kernel in os.listdir(f"{new_rootfs}/boot") + if kernel.startswith("vmlinuz") + ] + ) + == 0 + ): + output.error("new rootfs contains no kernel") + output.error("refusing to proceed with applying update") + exit(1) + + subprocess.run(["cp", "-ax", str(new_rootfs), "/.update_rootfs"]) + + replace_boot_files() + + print() diff --git a/usr/lib/akshara/utils/users.py b/usr/lib/akshara/utils/users.py new file mode 100644 index 0000000..1a8276d --- /dev/null +++ b/usr/lib/akshara/utils/users.py @@ -0,0 +1,209 @@ +def merge_passwd(new_rootfs) -> list: + """Merge /etc/passwd from host and new rootfs. + + Args: + new_rootfs: Path to new root filesystem. + + Returns: + A list of the newly generated passwd entries. + """ + + with open("/etc/passwd") as f: + current_passwd_entries = { + line.strip().split(":")[0]: line.strip() for line in f if line.strip() + } + + with open("/usr/etc/passwd") as f: + current_system_passwd_entries = { + line.strip().split(":")[0]: line.strip() for line in f if line.strip() + } + + with open(f"{new_rootfs}/etc/passwd") as f: + new_rootfs_passwd_entries = { + line.strip().split(":")[0]: line.strip() for line in f if line.strip() + } + + new_passwd_entries = list(new_rootfs_passwd_entries.values()) + + for user in set(current_passwd_entries.keys()) - set( + current_system_passwd_entries.keys() + ): + if ( + int(current_passwd_entries[user].split(":")[2]) >= 1000 + and user not in new_rootfs_passwd_entries.keys() + ): + new_passwd_entries.append(current_passwd_entries[user]) + + with open("/.new.etc/passwd", "w") as f: + for user in new_passwd_entries: + f.write(user + "\n") + + return new_passwd_entries + + +def merge_shadow(new_rootfs): + """Merge /etc/shadow from host and new rootfs. + + Args: + new_rootfs: Path to new root filesystem. + """ + + with open("/etc/passwd") as f: + current_passwd_entries = { + line.strip().split(":")[0]: line.strip() for line in f if line.strip() + } + + with open("/etc/shadow") as f: + current_shadow_entries = { + line.strip().split(":")[0]: line.strip() for line in f if line.strip() + } + + with open("/usr/etc/shadow") as f: + current_system_shadow_entries = { + line.strip().split(":")[0]: line.strip() for line in f if line.strip() + } + + with open(f"{new_rootfs}/etc/shadow") as f: + new_rootfs_shadow_entries = { + line.strip().split(":")[0]: line.strip() for line in f if line.strip() + } + + new_shadow_entries = list(new_rootfs_shadow_entries.values()) + + for user in set(current_shadow_entries.keys()) - set( + current_system_shadow_entries.keys() + ): + if ( + int(current_passwd_entries[user].split(":")[2]) >= 1000 + and user not in new_rootfs_shadow_entries.keys() + ): + new_shadow_entries.append(current_shadow_entries[user]) + + with open("/.new.etc/shadow", "w") as f: + for user in new_shadow_entries: + f.write(user + "\n") + + +def merge_group(new_rootfs, new_passwd_entries): + """Merge /etc/group from host and new rootfs. + + Args: + new_rootfs: Path to new root filesystem. + """ + + with open("/etc/group") as f: + current_group_entries = { + line.strip().split(":")[0]: line.strip() for line in f if line.strip() + } + + with open("/usr/etc/group") as f: + current_system_group_entries = { + line.strip().split(":")[0]: line.strip() for line in f if line.strip() + } + + with open(f"{new_rootfs}/etc/group") as f: + new_rootfs_group_entries = { + line.strip().split(":")[0]: line.strip() for line in f if line.strip() + } + + new_group_entries_names = list( + set(new_rootfs_group_entries.keys()) - set(current_system_group_entries.keys()) + ) + new_group_entries = [ + new_rootfs_group_entries[new_group_entries_name] + for new_group_entries_name in new_group_entries_names + ] + + for group in ( + set(current_system_group_entries.keys()) & set(new_rootfs_group_entries.keys()) + ) & set(current_group_entries.keys()): + old_group_entry = current_group_entries[group] + new_group_entry = new_rootfs_group_entries[group] + for member_user in old_group_entry.split(":")[3].split(","): + if member_user in [ + passwd_entry.split(":")[0] for passwd_entry in new_passwd_entries + ] and member_user not in new_group_entry.split(":")[3].split(","): + if new_group_entry.split(":")[3] == "": + new_group_entry += member_user + else: + new_group_entry += "," + member_user + new_group_entries.append(new_group_entry) + + for group in set(current_group_entries.keys()) - set( + current_system_group_entries.keys() + ): + if ( + int(current_group_entries[group].split(":")[2]) >= 1000 + and group not in new_rootfs_group_entries.keys() + ): + new_group_entries.append(current_group_entries[group]) + + with open("/.new.etc/group", "w") as f: + for group in new_group_entries: + f.write(group + "\n") + + +def merge_gshadow(new_rootfs, new_passwd_entries): + """Merge /etc/gshadow from host and new rootfs. + + Args: + new_rootfs: Path to new root filesystem. + """ + + with open("/etc/group") as f: + current_group_entries = { + line.strip().split(":")[0]: line.strip() for line in f if line.strip() + } + + with open("/etc/gshadow") as f: + current_gshadow_entries = { + line.strip().split(":")[0]: line.strip() for line in f if line.strip() + } + + with open("/usr/etc/gshadow") as f: + current_system_gshadow_entries = { + line.strip().split(":")[0]: line.strip() for line in f if line.strip() + } + + with open(f"{new_rootfs}/etc/gshadow") as f: + new_rootfs_gshadow_entries = { + line.strip().split(":")[0]: line.strip() for line in f if line.strip() + } + + new_gshadow_entries_names = list( + set(new_rootfs_gshadow_entries.keys()) + - set(current_system_gshadow_entries.keys()) + ) + new_gshadow_entries = [ + new_rootfs_gshadow_entries[new_gshadow_entries_name] + for new_gshadow_entries_name in new_gshadow_entries_names + ] + + for group in ( + set(current_system_gshadow_entries.keys()) + & set(new_rootfs_gshadow_entries.keys()) + ) & set(current_gshadow_entries.keys()): + old_gshadow_entry = current_gshadow_entries[group] + new_gshadow_entry = new_rootfs_gshadow_entries[group] + for member_user in old_gshadow_entry.split(":")[3].split(","): + if member_user in [ + passwd_entry.split(":")[0] for passwd_entry in new_passwd_entries + ] and member_user not in new_gshadow_entry.split(":")[3].split(","): + if new_gshadow_entry.split(":")[3] == "": + new_gshadow_entry += member_user + else: + new_gshadow_entry += "," + member_user + new_gshadow_entries.append(new_gshadow_entry) + + for group in set(current_gshadow_entries.keys()) - set( + current_system_gshadow_entries.keys() + ): + if ( + int(current_group_entries[group].split(":")[2]) >= 1000 + and group not in new_rootfs_gshadow_entries.keys() + ): + new_gshadow_entries.append(current_gshadow_entries[group]) + + with open("/.new.etc/gshadow", "w") as f: + for group in new_gshadow_entries: + f.write(group + "\n") diff --git a/usr/lib/dracut/modules.d/10akshara/handle-update.sh b/usr/lib/dracut/modules.d/10akshara/handle-update.sh new file mode 100755 index 0000000..fc292db --- /dev/null +++ b/usr/lib/dracut/modules.d/10akshara/handle-update.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +echo + +# Remove "$NEWROOT"/.successful-update if exists +rm -f "$NEWROOT"/.successful-update "$NEWROOT"/.update + +# Check if there is an available update +if [ -d "$NEWROOT"/.update_rootfs ]; then + # Available, rename old /usr and move new /usr to / + if [ -d "$NEWROOT"/.update_rootfs/usr ]; then + rm -rf "$NEWROOT"/.old.usr + mv "$NEWROOT"/usr "$NEWROOT"/.old.usr >/dev/null 2>&1 + mv "$NEWROOT"/.update_rootfs/usr "$NEWROOT"/usr + fi + + # Same for /etc + if [ -d "$NEWROOT"/.update_rootfs/etc ]; then + rm -rf "$NEWROOT"/.old.etc + mv "$NEWROOT"/etc "$NEWROOT"/.old.etc >/dev/null 2>&1 + mv "$NEWROOT"/.update_rootfs/etc "$NEWROOT"/etc + fi + + # Same for /var + if [ -d "$NEWROOT"/.update_rootfs/var ]; then + rm -rf "$NEWROOT"/.old.var + mv "$NEWROOT"/var "$NEWROOT"/.old.var >/dev/null 2>&1 + mv "$NEWROOT"/.update_rootfs/var "$NEWROOT"/var + fi + + rm -rf "$NEWROOT"/.old.update_rootfs + mv "$NEWROOT"/.update_rootfs "$NEWROOT"/.old.update_rootfs + touch "$NEWROOT"/.successful-update +fi + +mkdir -p "$NEWROOT"/.blendOS-overlays/usr +mount -t overlay overlay -o index=off -o metacopy=off -o ro,lowerdir="$NEWROOT"/usr,upperdir="$NEWROOT"/.blendOS-overlays/usr "$NEWROOT"/usr diff --git a/usr/lib/dracut/modules.d/10akshara/module-setup.sh b/usr/lib/dracut/modules.d/10akshara/module-setup.sh new file mode 100755 index 0000000..adedd7a --- /dev/null +++ b/usr/lib/dracut/modules.d/10akshara/module-setup.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +check() { + return 0 +} + +depends() { + echo base + return 0 +} + +installkernel() { + hostonly="" instmods overlay +} + +install() { + inst touch + + inst_hook pre-pivot 15 "$moddir/handle-update.sh" +}