fix(installer): test-build before replacing the active installation

Adapt the preflight protection from #128 without its firmware backup or
other installer policy changes. Build a clean private copy against the
running kernel before firmware replacement or DKMS removal. Treat log
initialization failure as an error and add mocked main-flow regressions.

Co-authored-by: rtk94 <richardtknepp@gmail.com>
(cherry picked from commit bee40068b9)
This commit is contained in:
Shen Mintao committed 2026-11-02 14:32:11 +08:00
1 parent f4065a8c89
commit 20fb7181e6
3 files changed
+160 -3

No files matched your search

+8
View File
@@ -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 `sudo pacman -Syu` yourself, reboot if the kernel was updated, and rerun the
installer with headers matching the running kernel. 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 The USB modules declare the complete firmware set shipped in `fw/`, including
configuration files and extension patches, so initramfs tools can include those configuration files and extension patches, so initramfs tools can include those
files when they include the modules. Rerun the installer after updating the files when they include the modules. Rerun the installer after updating the
+28 -3
View File
@@ -499,6 +499,30 @@ EOF
print_success "DKMS configuration created." 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 # DKMS Installation
############################################################################# #############################################################################
@@ -823,8 +847,8 @@ main() {
check_root check_root
# Initialize log file: owned by root, readable by all, never world-writable # Initialize log file: owned by root, readable by all, never world-writable
rm -f "$LOG_FILE" 2>/dev/null || true rm -f "$LOG_FILE"
install -m 0644 /dev/null "$LOG_FILE" 2>/dev/null || true install -m 0644 /dev/null "$LOG_FILE"
echo -e "${CYAN}" echo -e "${CYAN}"
echo "╔════════════════════════════════════════════════════════════════╗" echo "╔════════════════════════════════════════════════════════════════╗"
@@ -849,7 +873,8 @@ main() {
# Step 5: Check kernel build tree # Step 5: Check kernel build tree
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 install_firmware
# Step 7: Create DKMS configuration # Step 7: Create DKMS configuration
+124
View File
@@ -30,6 +30,8 @@ class InstallerTests(unittest.TestCase):
def run_installer_function(self, name, stubs="", after=""): def run_installer_function(self, name, stubs="", after=""):
# Extract only the functions under test; never execute main(). # Extract only the functions under test; never execute main().
names = ["run_logged", "cleanup_on_error", name] names = ["run_logged", "cleanup_on_error", name]
if name == "main" and re.search(r"(?m)^preflight_build\(\)", SOURCE):
names.insert(-1, "preflight_build")
functions = [] functions = []
for function in names: for function in names:
match = re.search(r"(?ms)^" + function + r"\(\) \{.*?^\}", SOURCE) match = re.search(r"(?ms)^" + function + r"\(\) \{.*?^\}", SOURCE)
@@ -42,6 +44,10 @@ class InstallerTests(unittest.TestCase):
{SHELL_OPTIONS} {SHELL_OPTIONS}
SCRIPT_DIR={shlex.quote(str(self.root))} SCRIPT_DIR={shlex.quote(str(self.root))}
LOG_FILE="$SCRIPT_DIR/install.log" 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_NAME=aic8800
DRV_VERSION=1.0.0 DRV_VERSION=1.0.0
MODULE_NAME=aic8800_fdrv MODULE_NAME=aic8800_fdrv
@@ -50,6 +56,7 @@ print_info() {{ echo "$*"; }}
print_success() {{ echo "$*"; }} print_success() {{ echo "$*"; }}
print_warning() {{ echo "$*"; }} print_warning() {{ echo "$*"; }}
print_error() {{ echo "$*"; }} print_error() {{ echo "$*"; }}
log_message() {{ :; }}
dkms() {{ echo 'aic8800/1.0.0: installed'; }} dkms() {{ echo 'aic8800/1.0.0: installed'; }}
lsmod() {{ :; }} lsmod() {{ :; }}
aic_zlp_target_present() {{ return 1; }} aic_zlp_target_present() {{ return 1; }}
@@ -132,6 +139,123 @@ cleanup_legacy_bluetooth_config() {{ echo LEGACY_CLEANUP; }}
self.assertFalse(old.exists()) self.assertFalse(old.exists())
self.assertEqual((old.parent / "new.bin").read_bytes(), b"new") 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__": if __name__ == "__main__":
unittest.main(verbosity=2) unittest.main(verbosity=2)