Refactoring Part 5
crew remove
Know Thy Enemy
Here’s the starting point, with some function calls inlined:
def remove(pkg_name)
# make sure the package is actually installed
unless @device[:installed_packages].any? { |pkg| pkg[:name] == pkg_name } || File.file?(File.join(CREW_META_PATH, "#{pkg_name}.filelist"))
puts "Package #{pkg_name} isn't installed.".lightred
return
end
# Perform any operations required prior to package removal.
search pkg_name, true
@pkg.preremove unless @in_fixup
# Preserve CREW_ESSENTIAL_FILES and make sure they are real files
# and not symlinks, because preserving symlinked libraries does not
# prevent breakage.
CREW_ESSENTIAL_FILES = `LD_TRACE_LOADED_OBJECTS=1 #{CREW_PREFIX}/bin/ruby`.scan(/\t([^ ]+)/).flatten +
%w[libzstd.so.1 libstdc++.so.6]
CREW_ESSENTIAL_FILES.uniq!
CREW_ESSENTIAL_FILES.each do |file|
next unless File.symlink?("#{CREW_LIB_PREFIX}/#{file}")
canonicalized_file = `readlink -m #{CREW_LIB_PREFIX}/#{file}`.chomp
if File.file?(canonicalized_file) && canonicalized_file.include?(CREW_PREFIX)
puts "Replacing symlinked essential #{file} with hard link to #{canonicalized_file} to avoid breakage.".lightblue if @opt_verbose
FileUtils.ln(canonicalized_file, "#{CREW_LIB_PREFIX}/#{file}", force: true)
end
end
conflicts = []
if File.file?("#{Dir.pwd}/filelist")
if File.file?(File.join(CREW_META_PATH, "#{pkg_name}.filelist"))
puts 'Checking for conflicts with files from installed packages...'.orange
conflictscmd = `grep --exclude=#{File.join(CREW_META_PATH, "#{pkg_name}.filelist")} --exclude=#{CREW_META_PATH}/\\\*_build.filelist -Fxf #{Dir.pwd}/filelist #{CREW_META_PATH}/*.filelist`
conflicts = conflictscmd.gsub(/(\.filelist|#{CREW_META_PATH})/, '').split("\n")
conflicts.reject!(&:empty?)
end
elsif File.file?(File.join(CREW_META_PATH, "#{pkg_name}.filelist"))
puts "Checking for conflicts of #{pkg_name} with files from installed packages...".orange
conflictscmd = `grep --exclude=#{File.join(CREW_META_PATH, "#{pkg_name}.filelist")} --exclude=#{CREW_META_PATH}/\\\*_build.filelist -Fxf #{File.join(CREW_META_PATH, "#{pkg_name}.filelist")} #{CREW_META_PATH}/*.filelist`
conflicts = conflictscmd.gsub(/(\.filelist|#{CREW_META_PATH})/, '').split("\n")
conflicts.reject!(&:empty?)
end
if conflicts.any?
puts 'There is a conflict with the same file in another package:'.orange
puts conflicts.to_s.orange
end
conflicts.map! { |x| x.to_s.partition(':').last}
# if the filelist exists, remove the files and directories installed by the package
if File.file?(File.join(CREW_META_PATH, "#{pkg_name}.filelist"))
Dir.chdir CREW_CONFIG_PATH do
# remove all files installed by the package
File.foreach("meta/#{pkg_name}.filelist", chomp: true) do |line|
# Do not remove essential files which crew (and dependencies)
# rely on, especially during package upgrades or reinstalls.
# These essential files are enumerated in const.rb as
# CREW_ESSENTIAL_FILES.
if CREW_ESSENTIAL_FILES.include?(File.basename(line))
puts "Removing #{line} will break crew. It was #{'NOT'.lightred} deleted." if @opt_verbose
else
puts "Removing file #{line}".lightred if @opt_verbose
puts "filelist contains #{line}".lightred if @opt_verbose && !line.include?(CREW_PREFIX)
if line.start_with?(CREW_PREFIX)
if conflicts.include?(line)
puts "#{line} is in another package. It will not be removed during the removal of #{pkg_name}".orange
else
FileUtils.rm_rf line
end
end
end
end
# remove all directories installed by the package
File.foreach("meta/#{pkg_name}.directorylist", chomp: true) do |line|
puts "directorylist contains #{line}".lightred if @opt_verbose && !line.include?(CREW_PREFIX)
next unless Dir.exist?(line) && Dir.empty?(line) && line.include?(CREW_PREFIX)
puts "Removing directory #{line}".lightred if @opt_verbose
FileUtils.rmdir(line)
end
# remove the file and directory list
FileUtils.rm_f Dir["meta/#{pkg_name}.{file,directory}list"]
end
end
# remove from installed packages
puts "Removing package #{pkg_name}".lightred if @opt_verbose
@device[:installed_packages].delete_if { |elem| elem[:name] == pkg_name }
# update the device manifest
File.write "#{CREW_CONFIG_PATH}/device.json", JSON.pretty_generate(JSON.parse(@device.to_json))
search pkg_name, true
@pkg.remove unless @in_fixup
puts "#{pkg_name.capitalize} removed!".lightgreen
end
Divide && Conquer
With the install check, there’s no need to also check for the filelist, since we do it later. Also, let’s swap to checking via .none?
. It’s not slower because we only expect one match, and its a little clearer. Oh, and we need to load device.json
instead of relying on the @device
instance variable.
device_json = JSON.load_file(File.join(CREW_CONFIG_PATH, 'device.json'))
# Make sure the package is actually installed before we attempt to remove it.
if device_json['installed_packages'].none? { |entry| entry['name'] == pkg.name }
puts "Package #{pkg.name} isn't installed.".lightred
return
end
@in_fixup
is there to handle the case of removing a package which doesn’t have a package file anymore, so we can’t call its preremove
or remove
methods. I don’t like it, though, and now that we’re passing the loaded package object directly to remove, we can just create a minimal object with stub methods and pass that, which looks something like this:
# Create a minimal Package object and pass it to Command.remove
pkg_object = Package
pkg_object.instance_eval do
self.name = pkg[:pkg_name]
def self.preremove; end
def self.remove; end
end
Command.remove(pkg_object, @opt_verbose)
CREW_ESSENTIAL_FILES
, while perfectly functional, is just not a very nice way of checking that we aren’t removing any critical libraries.
Instead of finding the currently loaded libraries at runtime and ensuring they aren’t deleted, why don’t we just find the packages providing those libraries and refuse to remove them?
# Don't remove any of the packages ruby (and thus crew) needs to run.
if %w[gcc_lib glibc gmp ruby zlibpkg zstd].include?(pkg.name)
puts "Refusing to remove essential package #{pkg.name}.".lightred
return
end
As a bonus, we don’t have to mess around with linking files anymore, and we avoid the problems that could come with partially removing libraries.
The conflict detection code only looks terrible because I’ve inlined the entire function, which has a lot more responsiblities in its original context. All we need it to do here is tell us if a file we’re about to remove is provided by any other package. We’ll get to the exact implementation of this later.
Gidorah
There are three main components to the actual removal code:
- The removal of files (Ichi)
- The removal of directories (Ni)
- The removal of the filelist and directorylist (San)
Before we tackle that, though, let’s clean up that check for the filelist– the last thing this block needs is more indentation.
# We can't remove a package if we don't have the filelist.
unless File.file?(File.join(CREW_META_PATH, "#{pkg.name}.filelist"))
puts "Unable to remove package #{pkg.name} as it does not have a filelist.".lightred
return
end
Ooh, an error message! Fancy.
Ichi’s certainly a beast to be reckoned with, but the desired behavior isn’t too bad.
We’ve already got the CREW_ESSENTIAL_FILES
check handled elsewhere, so we can drop that if/else
and save some indentation while we’re at it. Dropping some of the verbosity leaves us with some much cleaner behavior. All that we’re doing at this point is removing files as long as they start with CREW_PREFIX
and aren’t found in another package.
# Remove all files installed by the package.
File.foreach(File.join(CREW_META_PATH, "#{pkg.name}.filelist"), chomp: true) do |line|
next unless line.start_with?(CREW_PREFIX)
if system("grep --exclude #{pkg.name}.filelist -Fxq #{line} ./meta/*.filelist")
puts "#{line} is in another package. It will not be removed during the removal of #{pkg.name}.".orange
else
puts "Removing file #{line}".yellow if verbose
FileUtils.remove_file line
end
end
Honestly, Ni is already pretty well done– dropping some extra verbosity is really the only notable thing here.
# Remove all directories installed by the package.
File.foreach(File.join(CREW_META_PATH, "#{pkg.name}.directorylist"), chomp: true) do |line|
next unless Dir.exist?(line) && Dir.empty?(line) && line.include?(CREW_PREFIX)
puts "Removing directory #{line}".yellow if verbose
Dir.rmdir line
end
Kevin is, well, Kevin.
# Remove the file and directory lists.
FileUtils.remove_file File.join(CREW_META_PATH, "#{pkg.name}.filelist")
FileUtils.remove_file File.join(CREW_META_PATH, "#{pkg.name}.directorylist")
Apex
Putting it all together, here’s what we get.
require 'fileutils'
require 'json'
require_relative '../lib/const'
class Command
def self.remove(pkg, verbose)
device_json = JSON.load_file(File.join(CREW_CONFIG_PATH, 'device.json'))
# Make sure the package is actually installed before we attempt to remove it.
if device_json['installed_packages'].none? { |entry| entry['name'] == pkg.name }
puts "Package #{pkg.name} isn't installed.".lightred
return
end
# Don't remove any of the packages ruby (and thus crew) needs to run.
if %w[gcc_lib glibc gmp ruby zlibpkg zstd].include?(pkg.name)
puts "Refusing to remove essential package #{pkg.name}.".lightred
return
end
# We can't remove a package if we don't have the filelist.
unless File.file?(File.join(CREW_META_PATH, "#{pkg.name}.filelist"))
puts "Unable to remove package #{pkg.name} as it does not have a filelist.".lightred
return
end
# Perform any operations required prior to package removal.
pkg.preremove
# Remove the files and directories installed by the package.
Dir.chdir CREW_CONFIG_PATH do
# Remove all files installed by the package.
File.foreach(File.join(CREW_META_PATH, "#{pkg.name}.filelist"), chomp: true) do |line|
next unless line.start_with?(CREW_PREFIX)
if system("grep --exclude #{pkg.name}.filelist -Fxq #{line} ./meta/*.filelist")
puts "#{line} is in another package. It will not be removed during the removal of #{pkg.name}.".orange
else
puts "Removing file #{line}".yellow if verbose
FileUtils.remove_file line
end
end
# Remove all directories installed by the package.
File.foreach(File.join(CREW_META_PATH, "#{pkg.name}.directorylist"), chomp: true) do |line|
next unless Dir.exist?(line) && Dir.empty?(line) && line.include?(CREW_PREFIX)
puts "Removing directory #{line}".yellow if verbose
Dir.rmdir line
end
# Remove the file and directory lists.
FileUtils.remove_file File.join(CREW_META_PATH, "#{pkg.name}.filelist")
FileUtils.remove_file File.join(CREW_META_PATH, "#{pkg.name}.directorylist")
end
# Remove the package from the list of installed packages in device.json.
puts "Removing package #{pkg_name} from device.json".yellow if verbose
device_json['installed_packages'].delete_if { |entry| entry['name'] == pkg.name }
# Update device.json with our changes.
File.write File.join(CREW_CONFIG_PATH, 'device.json'), JSON.pretty_generate(JSON.parse(device_json.to_json))
# Perform any operations required after package removal.
pkg.remove
puts "#{pkg.name} removed!".lightgreen
end
end
PR here.