diff --git a/bin/crew b/bin/crew index fc16c0820..08cccf083 100755 --- a/bin/crew +++ b/bin/crew @@ -352,6 +352,7 @@ def upgrade(*pkgs, build_from_source: false) end to_be_upgraded = [] + extra_deps = [] if pkgs.any? # check for specific package(s) @@ -372,6 +373,12 @@ def upgrade(*pkgs, build_from_source: false) return true end + # Check if there are any new dependencies + to_be_upgraded.each do |pkg_name| + search(pkg_name) + extra_deps += resolve_dependencies + end + # Eventually, we should have the upgrade order generated based upon an # analysis of the dependency hierarchy, to make sure that earlier # dependencies get upgraded first. @@ -387,10 +394,40 @@ def upgrade(*pkgs, build_from_source: false) rerun_upgrade = true end + puts <<~EOT + + The following package(s) will be upgraded: + + #{to_be_upgraded.join(' ')} + + EOT + + puts <<~EOT if extra_deps.any? + The following package(s) also need to be installed: + + #{extra_deps.join(' ')} + + EOT + + if @opt_force + puts 'Proceeding with package upgrade...'.orange + elsif !Package.agree_default_yes('Proceed') + abort 'No changes made.' + end + # install new dependencies (if any) - to_be_upgraded.each do |pkg_name| - search(pkg_name) - resolve_dependencies + extra_deps.each do |dep_to_install| + search dep_to_install + print_current_package + install(skip_postinstall: true) + end + + puts 'Performing post-install for new dependencies...'.lightblue + + # do post-install for new dependencies + extra_deps.each do |dep_to_postinstall| + search dep_to_postinstall + post_install end puts 'Updating packages...' @@ -398,13 +435,12 @@ def upgrade(*pkgs, build_from_source: false) # upgrade packages to_be_upgraded.each do |pkg_name| search(pkg_name) - print_current_package @pkg.build_from_source = (build_from_source || CREW_BUILD_FROM_SOURCE) puts "Updating #{@pkg.name}..." if CREW_VERBOSE @pkg.in_upgrade = true - resolve_dependencies_and_install + resolve_dependencies_and_install(no_advisory: true) end if rerun_upgrade @@ -1067,24 +1103,59 @@ def install_package(pkgdir) end end -def resolve_dependencies_and_install - @resolve_dependencies_and_install = 1 - +def resolve_dependencies_and_install(no_advisory: false) # Process preflight block to see if package should even # be downloaded or installed. pre_flight begin - origin = @pkg.name + to_install = resolve_dependencies + [@pkg.name] + free_space = `df --output=avail #{CREW_PREFIX}`.lines(chomp: true).last.to_i * 1024 + install_size = to_install.sum do |pkg| + filelist = "#{CREW_LIB_PATH}/manifest/#{ARCH}/#{pkg[0]}/#{pkg}.filelist" + if File.exist?(filelist) + ConvenienceFunctions.read_filelist(filelist)[0] + else + 0 + end + end - @to_postinstall = [] - resolve_dependencies + if free_space < install_size + abort <<~EOT.chomp.lightred + #{@pkg.name.capitalize} needs #{MiscFunctions.human_size(install_size)} of disk space to install. - search origin, silent: true - install - @to_postinstall.append(@pkg.name) - @to_postinstall.each do |dep| - search dep + However, only #{MiscFunctions.human_size(free_space)} of free disk space is available currently. + EOT + end + + unless no_advisory + puts <<~EOT + + The following package(s) will be installed: + + #{to_install.join(' ')} + + After installation, #{MiscFunctions.human_size(install_size)} of extra disk space will be taken. (#{MiscFunctions.human_size(free_space)} of free disk space available) + + EOT + + if @opt_force + puts 'Proceeding with package installation...'.orange + elsif !Package.agree_default_yes('Proceed') + abort 'No changes made.' + end + end + + to_install.each do |pkg_to_install| + search pkg_to_install + print_current_package + install(skip_postinstall: true) + end + + puts 'Performing post-install for packages...'.lightblue + + to_install.each do |pkg_to_postinstall| + search pkg_to_postinstall post_install end rescue InstallError => e @@ -1135,80 +1206,44 @@ def resolve_dependencies_and_install end end puts "#{@pkg.name.capitalize} installed!".lightgreen - @resolve_dependencies_and_install = 0 end def resolve_dependencies - @dependencies = @pkg.get_deps_list(return_attr: true) + package_copy_prompt = <<~EOT.chomp + The package file for %s, which is a required dependency to build #{@pkg.name} only exists in #{CREW_LOCAL_REPO_ROOT}/packages/ . + Is it ok to copy it to #{CREW_PACKAGES_PATH} so that the build can continue? + EOT - # compare dependency version with required range (if installed) - @dependencies.each do |dep| - dep_name = dep.keys[0] - dep_info = @device[:installed_packages].select { |pkg| pkg[:name] == dep_name }[0] - - # skip if dependency is not installed - next unless dep_info - - _tags, version_check = dep.values[0] - installed_version = dep_info[:version] - - next unless version_check - - # abort if the range is not fulfilled - abort unless version_check.call(installed_version) - end + dependencies = @pkg.get_deps_list(return_attr: true) # leave only dependency names (remove all package attributes returned by @pkg.get_deps_list) - @dependencies.map!(&:keys).flatten! + dependencies.map!(&:keys).flatten! # abort & identify incompatible dependencies. - @dependencies.each do |dep| + dependencies.each do |dep| abort "Some dependencies e.g., #{dep}, are not compatible with your device architecture (#{ARCH}). Unable to continue.".lightred unless PackageUtils.compatible?(Package.load_package("#{CREW_PACKAGES_PATH}/#{dep}.rb")) end + # leave only not installed packages in dependencies - @dependencies.reject! { |dep_name| @device[:installed_packages].any? { |pkg| pkg[:name] == dep_name } } + dependencies.reject! { |dep_name| @device[:installed_packages].any? { |pkg| pkg[:name] == dep_name } } - # run preflight check for dependencies - @dependencies.each do |dep_name| - Package.load_package(File.join(CREW_PACKAGES_PATH, "#{dep_name}.rb")).preflight - end + dependencies.each do |dep| + dep_file = File.join(CREW_PACKAGES_PATH, "#{dep}.rb") - return if @dependencies.empty? - - puts 'The following packages also need to be installed: ' - - @dependencies.each do |dep| - FileUtils.cp "#{CREW_LOCAL_REPO_ROOT}/packages/#{dep}.rb", CREW_PACKAGES_PATH if !File.file?(File.join(CREW_PACKAGES_PATH, "#{dep}.rb")) && File.file?(File.join(CREW_LOCAL_REPO_ROOT, "packages/#{dep}.rb")) && (@opt_force || Package.agree_default_yes("The package file for #{dep}, which is a required dependency to build #{@pkg.name} only exists in #{CREW_LOCAL_REPO_ROOT}/packages/ . Is it ok to copy it to #{CREW_PACKAGES_PATH} so that the build can continue?")) - abort "Dependency #{dep} for #{@pkg.name} was not found.".lightred unless File.file?(File.join(CREW_PACKAGES_PATH, "#{dep}.rb")) - end - - puts @dependencies.join(' ') - - if @opt_force - puts 'Proceeding with dependency package installation...'.orange - elsif !Package.agree_default_yes('Proceed') - abort 'No changes made.' - end - - @dependencies.each do |dep| - search dep - print_current_package - install - end - if @resolve_dependencies_and_install.eql?(1) || @resolve_dependencies_and_build.eql?(1) - @to_postinstall = @dependencies - else - # Make sure the sommelier postinstall happens last so the messages - # from that are not missed by users. - @dependencies.partition { |v| v != 'sommelier' }.reduce(:+) - @dependencies.each do |dep| - search dep - post_install + # copy package script from CREW_LOCAL_REPO_ROOT if necessary + unless File.exist?(dep_file) + if File.exist?("#{CREW_LOCAL_REPO_ROOT}/packages/#{dep}.rb") && (@opt_force || Package.agree_default_yes(package_copy_prompt % dep)) + FileUtils.cp "#{CREW_LOCAL_REPO_ROOT}/packages/#{dep}.rb", dep_file + elsif !File.exist?(dep_file) + abort "Dependency #{dep} for #{@pkg.name} was not found.".lightred + end end end + + return dependencies end -def install +def install(skip_postinstall: false) @pkg.in_install = true if !@pkg.in_upgrade && PackageUtils.installed?(@pkg.name) && !@pkg.superclass.to_s == 'RUBY' puts "Package #{@pkg.name} already installed, skipping...".lightgreen @@ -1281,10 +1316,8 @@ def install install_package dest_dir end - unless (@resolve_dependencies_and_install == 1) || (@resolve_dependencies_and_build == 1) - # perform post-install process - post_install - end + # perform post-install process + post_install unless skip_postinstall end install_end_time = Time.now.to_i @@ -1302,20 +1335,63 @@ def install end def resolve_dependencies_and_build - @resolve_dependencies_and_build = 1 - - @to_postinstall = [] begin origin = @pkg.name # mark current package as which is required to compile from source @pkg.build_from_source = true - resolve_dependencies - @to_postinstall.each do |dep| - search dep + + dependencies = resolve_dependencies + free_space = `df --output=avail #{CREW_PREFIX}`.lines(chomp: true).last.to_i * 1024 + install_size = dependencies.sum do |pkg| + filelist = "#{CREW_LIB_PATH}/manifest/#{ARCH}/#{pkg[0]}/#{pkg}.filelist" + if File.exist?(filelist) + ConvenienceFunctions.read_filelist(filelist)[0] + else + 0 + end + end + + if free_space < install_size + abort <<~EOT.chomp.lightred + #{@pkg.name.capitalize} needs #{MiscFunctions.human_size(install_size)} of disk space to install. + + However, only #{MiscFunctions.human_size(free_space)} of free disk space is available currently. + EOT + end + + puts <<~EOT + + In order to build #{origin}, the following package(s) also need to be installed: + + #{dependencies.join(' ')} + + After installation, #{MiscFunctions.human_size(install_size)} of extra disk space will be taken. (#{MiscFunctions.human_size(free_space)} of free disk space available) + + EOT + + if @opt_force + puts 'Proceeding with dependency installation...'.orange + elsif !Package.agree_default_yes('Proceed') + abort 'No changes made.' + end + + # install dependencies + dependencies.each do |dep_to_install| + search dep_to_install + print_current_package + install(skip_postinstall: true) + end + + puts 'Performing post-install for dependencies...'.lightblue + + # run postinstall for dependencies + dependencies.each do |dep_to_postinstall| + search dep_to_postinstall post_install end - search origin, silent: true + + search @pkg.name, silent: true build_package CREW_LOCAL_BUILD_DIR rescue InstallError => e abort "#{@pkg.name} failed to build: #{e}".lightred @@ -1327,7 +1403,6 @@ def resolve_dependencies_and_build end end puts "#{@pkg.name.capitalize} is built!".lightgreen - @resolve_dependencies_and_build = 0 end def build_package(crew_archive_dest) diff --git a/lib/const.rb b/lib/const.rb index 12d630701..e2cff81e0 100644 --- a/lib/const.rb +++ b/lib/const.rb @@ -4,7 +4,7 @@ require 'etc' require 'open3' OLD_CREW_VERSION ||= defined?(CREW_VERSION) ? CREW_VERSION : '1.0' -CREW_VERSION ||= '1.65.1' unless defined?(CREW_VERSION) && CREW_VERSION == OLD_CREW_VERSION +CREW_VERSION ||= '1.65.2' unless defined?(CREW_VERSION) && CREW_VERSION == OLD_CREW_VERSION # Kernel architecture. KERN_ARCH ||= Etc.uname[:machine] diff --git a/lib/convenience_functions.rb b/lib/convenience_functions.rb index d6e6b29c8..0ebccf0f0 100644 --- a/lib/convenience_functions.rb +++ b/lib/convenience_functions.rb @@ -40,6 +40,17 @@ class ConvenienceFunctions return JSON.load_file(File.join(CREW_CONFIG_PATH, 'device.json'), symbolize_names: true).transform_values! { |val| val.is_a?(String) ? val.to_sym : val } end + def self.read_filelist(path) + filelist = File.readlines(path, chomp: true) + + if filelist.first.start_with?('# Total size') + total_size, *contents = filelist + return [total_size[/Total size: (\d+)/, 1].to_i, contents] + else + return [0, *filelist] + end + end + def self.save_json(json_object) crewlog 'Saving device.json...' begin diff --git a/lib/package.rb b/lib/package.rb index e11f7fb11..e93c00de1 100644 --- a/lib/package.rb +++ b/lib/package.rb @@ -122,10 +122,10 @@ class Package end def self.get_deps_list(pkg_name = name, return_attr: false, hash: false, include_build_deps: 'auto', include_self: false, - pkg_tags: [], ver_check: nil, highlight_build_deps: true, exclude_buildessential: false, top_level: true) + pkg_tags: [], highlight_build_deps: true, exclude_buildessential: false, top_level: true) # get_deps_list: get dependencies list of pkg_name (current package by default) # - # pkg_name: package to check dependencies, current package by default + # pkg_name: package to check dependencies, current package by default # return_attr: return package attribute (tags and version lambda) also # hash: return result in nested hash, used by `print_deps_tree` (`bin/crew`) # @@ -160,7 +160,7 @@ class Package end # Parse dependencies recursively. - expanded_deps = deps.uniq.map do |dep, (dep_tags, ver_check)| + expanded_deps = deps.uniq.map do |dep, dep_tags| # Check build dependencies only if building from source is needed/specified. # Do not recursively find :build based build dependencies. next unless (include_build_deps == true && @crew_current_package == pkg_obj.name) || @@ -175,7 +175,7 @@ class Package # Check dependency by calling this function recursively. next \ send( - __method__, dep, pkg_tags: tags, ver_check:, include_self: true, top_level: false, + __method__, dep, pkg_tags: tags, include_self: true, top_level: false, hash:, return_attr:, include_build_deps:, highlight_build_deps:, exclude_buildessential: ) elsif hash && top_level @@ -203,7 +203,7 @@ class Package elsif include_self # Return pkg_name itself if this function is called as a recursive loop (see `expanded_deps`). if return_attr - return [expanded_deps, { pkg_name => [pkg_tags, ver_check] }].flatten + return [expanded_deps, { pkg_name => pkg_tags }].flatten else return [expanded_deps, pkg_name].flatten end @@ -288,10 +288,9 @@ class Package puts tree_view end - def self.depends_on(dependency, ver_range = nil) + def self.depends_on(dependency) @dependencies ||= {} - ver_check = nil - dep_tags = [] + dep_tags = [] # Add element in "[ name, [ tag1, tag2, ... ] ]" format. if dependency.is_a?(Hash) @@ -305,30 +304,7 @@ class Package dep_name = dependency end - # Process dependency version range if specified. - # example: - # depends_on name, '>= 1.0' - # - # Operators can be: '>=', '==', '<=', '<', or '>' - if ver_range - operator, target_ver = ver_range.split(' ', 2) - - # lambda for comparing the given range with installed version - ver_check = lambda do |installed_ver| - unless Gem::Version.new(installed_ver).send(operator.to_sym, Gem::Version.new(target_ver)) - # Print error if the range is not fulfilled. - warn <<~EOT.lightred - Package #{name} depends on '#{dep_name}' (#{operator} #{target_ver}), however version '#{installed_ver}' is currently installed :/ - - Run `crew update && crew upgrade` and try again? - EOT - return false - end - return true - end - end - - @dependencies.store(dep_name, [dep_tags, ver_check]) + @dependencies.store(dep_name, dep_tags) end def self.binary?(architecture) = !@build_from_source && @binary_sha256&.key?(architecture)