diff --git a/INSTALL_SCRIPT.md b/INSTALL_SCRIPT.md index 5ba31c3..80eabc8 100644 --- a/INSTALL_SCRIPT.md +++ b/INSTALL_SCRIPT.md @@ -35,6 +35,14 @@ cannot download dependencies because the database is stale, run `sudo pacman -Syu` yourself, reboot if the kernel was updated, and rerun the installer with headers matching the running kernel. +Before replacing installed firmware or DKMS modules, the installer cleans and +test-builds a temporary copy of the driver for the running kernel. A test-build +failure leaves the existing firmware, DKMS registration and installed source +untouched. A successful installation compiles again through DKMS, so this check +adds build time. Logs are written to `/var/log/aic8800-install.log` with mode +`0644` after the root check. This check does not provide automatic rollback if a +later installation step fails. + The USB modules declare the complete firmware set shipped in `fw/`, including configuration files and extension patches, so initramfs tools can include those files when they include the modules. Rerun the installer after updating the diff --git a/install.sh b/install.sh index 1a9e6ab..6193589 100755 --- a/install.sh +++ b/install.sh @@ -499,6 +499,30 @@ EOF print_success "DKMS configuration created." } +############################################################################# +# Pre-flight Build +############################################################################# + +preflight_build() { + local build_root kver + kver="$(uname -r)" + print_step "Test-building the driver for $kver before replacing the existing installation..." + + build_root=$(mktemp -d "${TMPDIR:-/tmp}/aic8800-preflight.XXXXXX") + # Use a private copy and clean it so a previous build cannot hide an error. + if cp -r "${SCRIPT_DIR}/drivers" "$build_root/" >> "$LOG_FILE" 2>&1 && + make -C "$build_root/drivers/aic8800" KVER="$kver" KDIR="/lib/modules/$kver/build" clean >> "$LOG_FILE" 2>&1 && + make -C "$build_root/drivers/aic8800" KVER="$kver" KDIR="/lib/modules/$kver/build" >> "$LOG_FILE" 2>&1; then + rm -rf "$build_root" + print_success "Test build succeeded." + else + rm -rf "$build_root" + print_error "Test build failed for kernel $kver. Existing firmware and DKMS installation were not changed." + echo "See $LOG_FILE for the compiler output." + return 1 + fi +} + ############################################################################# # DKMS Installation ############################################################################# @@ -823,8 +847,8 @@ main() { check_root # Initialize log file: owned by root, readable by all, never world-writable - rm -f "$LOG_FILE" 2>/dev/null || true - install -m 0644 /dev/null "$LOG_FILE" 2>/dev/null || true + rm -f "$LOG_FILE" + install -m 0644 /dev/null "$LOG_FILE" echo -e "${CYAN}" echo "╔════════════════════════════════════════════════════════════════╗" @@ -849,7 +873,8 @@ main() { # Step 5: Check kernel build tree check_kernel_build_tree - # Step 6: Install firmware + # Step 6: Test-build before changing installed firmware or DKMS modules + preflight_build install_firmware # Step 7: Create DKMS configuration diff --git a/tests/installer/test_install.py b/tests/installer/test_install.py index 4a9e408..54ab95d 100644 --- a/tests/installer/test_install.py +++ b/tests/installer/test_install.py @@ -30,6 +30,8 @@ class InstallerTests(unittest.TestCase): def run_installer_function(self, name, stubs="", after=""): # Extract only the functions under test; never execute main(). names = ["run_logged", "cleanup_on_error", name] + if name == "main" and re.search(r"(?m)^preflight_build\(\)", SOURCE): + names.insert(-1, "preflight_build") functions = [] for function in names: match = re.search(r"(?ms)^" + function + r"\(\) \{.*?^\}", SOURCE) @@ -42,6 +44,10 @@ class InstallerTests(unittest.TestCase): {SHELL_OPTIONS} SCRIPT_DIR={shlex.quote(str(self.root))} LOG_FILE="$SCRIPT_DIR/install.log" +SRC_DIR="$SCRIPT_DIR/installed-source" +TMPDIR="$SCRIPT_DIR/tmp" +export TMPDIR +mkdir -p "$TMPDIR" DRV_NAME=aic8800 DRV_VERSION=1.0.0 MODULE_NAME=aic8800_fdrv @@ -50,6 +56,7 @@ print_info() {{ echo "$*"; }} print_success() {{ echo "$*"; }} print_warning() {{ echo "$*"; }} print_error() {{ echo "$*"; }} +log_message() {{ :; }} dkms() {{ echo 'aic8800/1.0.0: installed'; }} lsmod() {{ :; }} aic_zlp_target_present() {{ return 1; }} @@ -132,6 +139,123 @@ cleanup_legacy_bluetooth_config() {{ echo LEGACY_CLEANUP; }} self.assertFalse(old.exists()) self.assertEqual((old.parent / "new.bin").read_bytes(), b"new") + def prepare_preflight(self): + source = self.root / "drivers" / "aic8800" / "source.c" + source.parent.mkdir(parents=True) + source.write_bytes(b"source") + old = self.firmware / "aic8800D80" / "old.bin" + old.parent.mkdir() + old.write_bytes(b"installed firmware") + installed = self.root / "installed-source" / "revision" + installed.parent.mkdir() + installed.write_bytes(b"installed source") + (self.root / "dkms-state").write_bytes(b"installed modules") + return r''' +uname() { echo test-kernel; } +check_root() { :; } +check_secure_boot() { :; } +detect_package_manager() { DETECTED_PKG_MANAGER=apt; } +install_dependencies() { :; } +check_kernel_build_tree() { :; } +create_dkms_conf() { :; } +refresh_initramfs() { :; } +load_module() { :; } +verify_installation() { :; } +show_final_instructions() { echo FINAL_INSTRUCTIONS; } +make() { + echo "MAKE $*" >> "$SCRIPT_DIR/events" + [ "$(cat "$2/source.c")" = source ] || return 99 + case "$*" in + *' clean') return "${MAKE_CLEAN_STATUS:-0}" ;; + esac + echo built > "$2/source.c" + return "${MAKE_BUILD_STATUS:-0}" +} +install_firmware() { + echo FIRMWARE >> "$SCRIPT_DIR/events" + echo changed > "$SCRIPT_DIR/firmware/aic8800D80/old.bin" +} +install_via_dkms() { + echo DKMS >> "$SCRIPT_DIR/events" + echo changed > "$SRC_DIR/revision" + echo changed > "$SCRIPT_DIR/dkms-state" +} +''' + + def assert_installation_preserved(self, result): + self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual((self.firmware / "aic8800D80/old.bin").read_bytes(), b"installed firmware") + self.assertEqual((self.root / "installed-source/revision").read_bytes(), b"installed source") + self.assertEqual((self.root / "dkms-state").read_bytes(), b"installed modules") + events = self.root / "events" + if events.exists(): + self.assertNotIn("FIRMWARE", events.read_text()) + self.assertNotIn("DKMS", events.read_text()) + self.assertNotIn("FINAL_INSTRUCTIONS", result.stdout) + self.assertEqual(list((self.root / "tmp").iterdir()), []) + + def test_failed_preflight_preserves_firmware_source_and_dkms(self): + stubs = self.prepare_preflight() + "\nMAKE_BUILD_STATUS=17\n" + result = self.run_installer_function("main", stubs) + self.assert_installation_preserved(result) + self.assertIn("Test build failed", result.stdout) + self.assertIn("MAKE", (self.root / "events").read_text()) + self.assertEqual((self.root / "drivers/aic8800/source.c").read_bytes(), b"source") + + def test_preflight_copy_failure_preserves_installation(self): + stubs = self.prepare_preflight() + "\ncp() { return 18; }\n" + result = self.run_installer_function("main", stubs) + self.assert_installation_preserved(result) + self.assertFalse((self.root / "events").exists()) + + def test_preflight_clean_failure_preserves_installation(self): + stubs = self.prepare_preflight() + "\nMAKE_CLEAN_STATUS=19\n" + result = self.run_installer_function("main", stubs) + self.assert_installation_preserved(result) + self.assertEqual(len((self.root / "events").read_text().splitlines()), 1) + + def test_preflight_success_precedes_installation_and_cleans_temp(self): + result = self.run_installer_function("main", self.prepare_preflight()) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + events = (self.root / "events").read_text().splitlines() + self.assertEqual(len(events), 4) + self.assertTrue(events[0].endswith(" clean"), events) + self.assertTrue(events[1].startswith("MAKE "), events) + self.assertEqual(events[2:], ["FIRMWARE", "DKMS"]) + for line in events[:2]: + self.assertIn(str(self.root / "tmp"), line) + self.assertIn("KVER=test-kernel KDIR=/lib/modules/test-kernel/build", line) + self.assertEqual((self.root / "drivers/aic8800/source.c").read_bytes(), b"source") + self.assertEqual(list((self.root / "tmp").iterdir()), []) + self.assertIn("FINAL_INSTRUCTIONS", result.stdout) + + def test_log_replaces_symlink_without_overwriting_target(self): + stubs = self.prepare_preflight() + target = self.root / "keep.log" + target.write_bytes(b"keep this file") + (self.root / "install.log").symlink_to(target) + result = self.run_installer_function("main", stubs) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(target.read_bytes(), b"keep this file") + log = self.root / "install.log" + self.assertFalse(log.is_symlink()) + self.assertEqual(log.stat().st_mode & 0o777, 0o644) + + def test_root_check_precedes_log_changes(self): + stubs = self.prepare_preflight() + "\ncheck_root() { return 20; }\n" + log = self.root / "install.log" + log.write_bytes(b"previous log") + result = self.run_installer_function("main", stubs) + self.assert_installation_preserved(result) + self.assertEqual(log.read_bytes(), b"previous log") + self.assertFalse((self.root / "events").exists()) + + def test_log_creation_failure_stops_before_preflight(self): + stubs = self.prepare_preflight() + "\ninstall() { return 21; }\n" + result = self.run_installer_function("main", stubs) + self.assert_installation_preserved(result) + self.assertFalse((self.root / "events").exists()) + if __name__ == "__main__": unittest.main(verbosity=2)