agama/live/live-root/tmp/driver_cleanup.rb
Ladislav Slezák 655882fa54
Rename "root" to "live-root" to avoid confusion
...with "root/root".
2025-04-23 13:26:35 +02:00

177 lines
5.6 KiB
Ruby
Executable file

#! /usr/bin/env ruby
# This script removes not needed drivers from the Live ISO. It deletes the drivers which are either
# not relevant for the installer (sound cards, TV cards, joysticks, NFC...) or the hardware is
# obsolete and very likely not used in modern systems (PCMCIA, Appletalk...).
#
# By default the script runs in safe mode and only lists the drivers to delete, use the "--delete"
# argument to really delete the drivers.
#
# The script uses the "module.list" file from the installation-images package
# (https://github.com/openSUSE/installation-images/blob/master/etc/module.list). The file should be
# updated manually from time to time. Hot fixes or Agama specific changes should be added into the
# module.list.extra file.
#
# The file lists the drivers or whole directories which should be present in the installation
# system. If the line starts with "-" then that driver or directory should be removed. It is usually
# connected with the previous line, it allows to include a whole directory with drivers but delete
# just a specific driver or subdirectory below it.
#
# The file is actually a list of Perl regexps, hopefully only the basic regexp features which work
# also in Ruby will be ever used...
require "find"
require "shellwords"
# class holding the kernel driver data
class Driver
# the driver name, full file path, dependencies
attr_reader :name, :path, :deps
def initialize(name, path, deps)
@name = name
@path = path
@deps = deps
end
# load the kernel driver data from the given path recursively
def self.find(dir)
drivers = []
puts "Scanning kernel modules in #{dir}..."
return drivers unless File.directory?(dir)
Find.find(dir) do |path|
if File.file?(path) && path.end_with?(".ko", ".ko.xz", ".ko.zst")
drivers << Driver.from_file(path)
end
end
return drivers
end
# create a driver object from a kernel driver file
def self.from_file(file)
deps = `/usr/sbin/modinfo -F depends #{file.shellescape}`.chomp.split(",")
name = File.basename(file).sub(/\.ko(\.xz|\.zst|)\z/, "")
Driver.new(name, file, deps)
end
end
# Remove lines for other architectures than the current machine architecture. The arch specific
# lines start with the <$arch> line and end with the </$arch> line.
def arch_filter(lines)
# the current state for a finite-state machine with two states (inside or outside an arch tag)
skipping = false
# the current machine architecture
arch = `arch`.strip
# the architecture from the tag line
arch_tag = nil
lines.reject! do |line|
# opening arch tag
if line.match(/^\s*<\s*(\w+)\s*>\s*$/)
arch_tag = Regexp.last_match[1]
skipping = arch_tag != arch
# always remove the arch tag
next true
end
# closing arch tag, for simplicity let's assume it matches the previous opening tag, the tags
# cannot be nested and the input file is under our control and so we can be sure it is valid
if line.match(/^\s*<\/\s*\w+\s*>\s*$/)
skipping = false
# always remove the arch tag
next true
end
if skipping
puts "Ignoring #{arch_tag} specific line: #{line}"
end
skipping
end
end
# really delete or just do a smoke test?
do_delete = ARGV[0] == "--delete"
debug = ENV["DEBUG"] == "1"
# read the configuration files
# this file is a copy from https://github.com/openSUSE/installation-images/blob/master/etc/module.list
config = File.read(File.join(__dir__, "module.list")).split("\n")
# here are Agama specific overrides
config += File.read(File.join(__dir__, "module.list.extra")).split("\n")
# remove comments and empty lines
config.reject!{|l| l.empty? || l.start_with?("#")}
# process the architecture specific lines
arch_filter(config)
# split the list into keep and delete parts (starting with "-")
delete, keep = config.partition{|c| c.start_with?("-")}
# remove the delete prefix "-"
delete.map!{|l| l.delete_prefix("-")}
# convert to regular expressions
keep.map!{|l| Regexp.new(l)}
delete.map!{|l| Regexp.new(l)}
# in the Live ISO there should be just one kernel installed
dir = Dir["/lib/modules/*"].first
to_keep = []
to_delete = []
puts "Scanning kernel modules in #{dir}..."
Find.find(dir) do |path|
next unless File.file?(path) && path.end_with?(".ko", ".ko.xz", ".ko.zst")
driver = Driver.from_file(path)
kernel_path = path.delete_prefix(dir).delete_prefix("/")
if delete.any?{|d| d.match?(kernel_path)}
# deleted explicitly by config
to_delete << driver
elsif keep.any?{|k| k.match?(kernel_path)}
# included explicitly by config
to_keep << driver
else
# implicitly delete all unknown drivers not mentioned in the config
to_delete << driver
end
end
puts "Checking driver dependencies..."
# iteratively find the dependant drivers (dependencies of dependencies...), move the referenced
# drivers from the delete list to the keep list until no driver in the delete list is referenced
# from the keep list
loop do
referenced = to_delete.select do |dd|
to_keep.any?{|ad| ad.deps.include?(dd.name)}
end
# no dependencies, the end of the dependency chain reached
break if referenced.empty?
referenced.each do |d|
puts "Keep dependant driver #{d.path}"
end
# move the referenced drivers from the delete list to the keep list
to_keep += referenced
to_delete.reject!{|a| referenced.any?{|d| d.path == a.path}}
end
delete_drivers = to_delete.map(&:path)
puts "Found #{delete_drivers.size} drivers to delete"
puts delete_drivers if debug
File.delete(*delete_drivers) if do_delete
# Note: The module dependencies are updated by the config.sh script
# after decompressing the drivers.