#!/usr/bin/env python3 # femctl - a simple sysv cli bootscript manager # Copyright (C) 2025 Gabriel Di Martino # This program 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. # # This program 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 program. If not, see . import os import sys import subprocess import tarfile import urllib.request import difflib from pathlib import Path import re import json """Femctl: small utility frontend for BLFS bootscripts with remote overlays.""" BOOTSCRIPT_VER = "20250225" BLFS_URL = f"https://anduin.linuxfromscratch.org/BLFS/blfs-bootscripts/blfs-bootscripts-{BOOTSCRIPT_VER}.tar.xz" CACHE_DIR = Path.home() / ".blfs_cache" TARBALL = CACHE_DIR / "blfs-bootscripts.tar.xz" SRC_DIR = CACHE_DIR / f"blfs-bootscripts-{BOOTSCRIPT_VER}" OVERLAY_INDEX_URL = "https://rocketleaguechatp.duckdns.org/femctl/overlays.json" def check_root(): if os.geteuid() != 0: print("This script must be run as root!") sys.exit(1) def download_tarball(): CACHE_DIR.mkdir(parents=True, exist_ok=True) if not TARBALL.exists(): print(f"Downloading BLFS bootscripts to {TARBALL}...") urllib.request.urlretrieve(BLFS_URL, TARBALL) else: print(f"Using cached tarball at {TARBALL}") def extract_tarball(): if not SRC_DIR.exists(): print(f"Extracting tarball to {SRC_DIR}...") SRC_DIR.mkdir(parents=True) with tarfile.open(TARBALL, "r:xz") as tar: tar.extractall(path=CACHE_DIR) else: print(f"Using cached source at {SRC_DIR}") def parse_services(): makefile_path = SRC_DIR / "Makefile" if not makefile_path.exists(): print(f"Makefile not found in {SRC_DIR}, did extraction fail?") sys.exit(1) services = set() with open(makefile_path) as f: content = f.read() # Find install- and uninstall- targets for match in re.findall(r'(?:install|uninstall)-([a-zA-Z0-9_-]+)', content): services.add(match) return sorted(services) def suggest_service(name, services): matches = difflib.get_close_matches(name, services, n=1) if matches: print(f"{name} does not exist. Did you mean {matches[0]}?") else: print(f"{name} does not exist and no close match was found.") def run_make(service, action, services): if service not in services: suggest_service(service, services) return target = f"{action}-{service}" print(f"Running `make {target}`...") subprocess.run(["make", target], cwd=SRC_DIR, check=True) def print_version(): print("""Femctl version 1.0.2 Made by gabry for FemboyOS Credits to the BLFS authors for the BLFS bootscripts""") def print_help(): print("""Usage: femctl Commands: enable Enable a service (make install-{service}) disable Disable a service (make uninstall-{service}) version Shows version and credits help Show this message Example: femctl enable xdm femctl disable sshd""") # ----------------- remote overlay logic ----------------- def fetch_overlay_index(): """Fetch overlay index dynamically from server""" with urllib.request.urlopen(OVERLAY_INDEX_URL) as resp: return json.load(resp) def apply_remote_overlay_live(service, patch_name): """Download and apply a patch directly to /etc/init.d/{service}""" url = f"https://rocketleaguechatp.duckdns.org/femctl/patches/{service}/{patch_name}" target_script_dir = Path("/etc/init.d") target_script_path = target_script_dir / service if not target_script_path.exists(): print(f"Service script {target_script_path} not found!") return print(f"Downloading and applying overlay {patch_name} to {target_script_path}...") # download patch in memory with urllib.request.urlopen(url) as resp: patch_data = resp.read().decode("utf-8") # apply patch to /etc/init.d subprocess.run( ["patch", str(target_script_path)], input=patch_data, text=True, check=True ) def apply_service_overlays(service): """Fetch overlay index and apply all patches for a service""" index = fetch_overlay_index() if index.get("blfs_version") != BOOTSCRIPT_VER: print("Warning: overlay BLFS version mismatch") service_entry = index.get("overlays", {}).get(service, None) if not service_entry: return # no overlays for this service target = service_entry.get("target", service) # fallback to the service itself for patch_name in service_entry.get("patches", []): apply_remote_overlay_live(target, patch_name) # --------------------------------------------------------- def main(): check_root() if len(sys.argv) < 2 or sys.argv[1] in ("help", "-h", "--help"): print_help() return elif len(sys.argv) < 2 or sys.argv[1] in ("version"): print_version() return download_tarball() extract_tarball() services = parse_services() command = sys.argv[1] if len(sys.argv) < 3: print("Please specify a service name.") return service = sys.argv[2] if command == "enable": run_make(service, "install", services) apply_service_overlays(service) elif command == "disable": run_make(service, "uninstall", services) else: print_help() if __name__ == "__main__": main()