#!/usr/bin/env ruby require_relative '../lib/color' # Disallow sudo abort 'Chromebrew should not be run as root.'.lightred if Process.uid == 0 require 'find' require 'net/http' require 'uri' require 'digest/sha2' require 'json' require 'fileutils' require 'securerandom' require 'tmpdir' require_relative '../lib/const' require_relative '../lib/util' # Add lib to LOAD_PATH $LOAD_PATH.unshift "#{CREW_LIB_PATH}lib" DOC = < ... crew const [options] [ ...] crew deps [options] ... crew download [options] ... crew files [options] ... crew help [] crew install [options] [-k|--keep] [-s|--build-from-source] [-S|--recursive-build] ... crew list [options] (available|installed|compatible|incompatible) crew postinstall [options] ... crew reinstall [options] [-k|--keep] [-s|--build-from-source] [-S|--recursive-build] ... crew remove [options] ... crew search [options] [ ...] crew update [options] [] crew upgrade [options] [-k|--keep] [-s|--build-from-source] [ ...] crew whatprovides [options] ... -c --color Use colors even if standard out is not a tty. -d --no-color Disable colors even if standard out is a tty. -k --keep Keep the `CREW_BREW_DIR` (#{CREW_BREW_DIR}) directory. -L --license Display the crew license. -s --build-from-source Build from source even if pre-compiled binary exists. -S --recursive-build Build from source, including all dependencies, even if pre-compiled binaries exist. -v --verbose Show extra information. -V --version Display the crew version. -h --help Show this screen. version #{CREW_VERSION} DOCOPT CREW_LICENSE = <<~LICENSESTRING Copyright (C) 2021 Chromebrew Authors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html. LICENSESTRING # Set XZ_OPT environment variable for build command. # If CREW_XZ_OPT is defined, use it by default. Use `-7e`, otherwise. ENV["XZ_OPT"] = ENV['CREW_XZ_OPT'] || "-7e -T #{CREW_NPROC}" # If CURL environment variable exists use it in lieu of curl. CURL = ENV['CURL'] || 'curl' # All available crew commands. @cmds = ["build", "const", "deps", "download", "files", "help", "install", "list", "postinstall", "reinstall", "remove", "search", "update", "upgrade", "whatprovides"] # Parse arguments using docopt require_relative '../lib/docopt' begin args = Docopt::docopt(DOC) args[''] = args[''].map { |arg| arg.gsub('-','_') } if args[''] rescue Docopt::Exit => e if ARGV[0] then case ARGV[0] when '-V', '--version' puts CREW_VERSION exit 0 when '-L', '--license' puts CREW_LICENSE exit 0 end if ARGV[0] != '-h' and ARGV[0] != '--help' then puts "Could not understand \"crew #{ARGV.join(' ')}\".".lightred # Looking for similar commands if not @cmds.include?(ARGV[0]) then similar = @cmds.select {|word| edit_distance(ARGV[0], word) < 4} if not similar.empty? then puts 'Did you mean?' similar.each {|sug| puts " #{sug}"} end end end end puts e.message exit 1 end String.use_color = args["--color"] || !args["--no-color"] @opt_keep = args["--keep"] @opt_verbose = args["--verbose"] if @opt_verbose then @fileutils_verbose = true @verbose = 'v' @short_verbose = '-v' else @fileutils_verbose = false @verbose = '' @short_verbose = '' end @opt_src = args["--build-from-source"] @opt_recursive = args["--recursive-build"] @device = JSON.parse(File.read(CREW_CONFIG_PATH + 'device.json'), symbolize_names: true) #symbolize also values @device.each do |key, elem| @device[key] = @device[key].to_sym rescue @device[key] end def print_package(pkgPath, extra = false) pkgName = File.basename pkgPath, '.rb' begin set_package pkgName, pkgPath rescue => e puts "Error with #{pkgName}.rb: #{e}".red unless e.to_s.include?('uninitialized constant') end print_current_package extra end def print_current_package(extra = false) status = '' status = 'installed' if @device[:installed_packages].any? do |elem| elem[:name] == @pkg.name end status = 'incompatible' unless @device[:compatible_packages].any? do |elem| elem[:name] == @pkg.name end case status when 'installed' print @pkg.name.lightgreen when 'incompatible' print @pkg.name.lightred else print @pkg.name.lightblue end print ": #{@pkg.description}".lightblue if @pkg.description if extra puts "" puts @pkg.homepage if @pkg.homepage puts "Version: #{@pkg.version}" print "License: #{@pkg.license}" if @pkg.license end puts "" end def set_package(pkgName, pkgPath) begin require_relative pkgPath rescue SyntaxError => e puts "#{e.class}: #{e.message}".red end @pkg = Object.const_get(pkgName.capitalize) @pkg.build_from_source = true if @opt_recursive @pkg.name = pkgName end def list_packages Dir[CREW_PACKAGES_PATH + '*.rb'].each do |filename| print_package filename end end def list_available Dir[CREW_PACKAGES_PATH + '*.rb'].each do |filename| notInstalled = true pkgName = File.basename filename, '.rb' notInstalled = false if File.exist? CREW_META_PATH + pkgName + '.filelist' if notInstalled begin set_package pkgName, filename rescue => e puts "Error with #{pkgName}.rb: #{e}".red unless e.to_s.include?('uninitialized constant') end if @pkg.compatibility&.include? 'all' or @pkg.compatibility&.include? ARCH puts pkgName end end end end def list_installed unless @opt_verbose Dir[CREW_META_PATH + '*.directorylist'].sort.map do |f| File.basename(f, '.directorylist').lightgreen end else @installed_packages = [] @device[:installed_packages].each do |package| search package[:name], true @installed_packages.append(package[:name] + ' ' + package[:version].to_s) end @sorted_installed_packages = @installed_packages.sort @sorted_installed_packages.unshift('======= =======') @sorted_installed_packages.unshift('Package Version') @first_col_width = @sorted_installed_packages.map(&:split).map(&:first).max_by(&:size).size + 2 @sorted_installed_packages.map(&:strip).each do |line| puts "%-#{@first_col_width}s%s".lightgreen % line.split end puts end end def list_compatible(compat = true) Dir[CREW_PACKAGES_PATH + '*.rb'].each do |filename| pkgName = File.basename filename, '.rb' if @device[:compatible_packages].any? do |elem| elem[:name] == pkgName end if compat if ( File.exist? CREW_META_PATH + pkgName + '.filelist' ) puts pkgName.lightgreen else puts pkgName end end else puts pkgName.lightred unless compat end end end def generate_compatible puts 'Generating compatible packages...'.orange if @opt_verbose @device[:compatible_packages] = [] Dir[CREW_PACKAGES_PATH + '*.rb'].each do |filename| pkgName = File.basename filename, '.rb' begin set_package pkgName, filename rescue => e puts "Error with #{pkgName}.rb: #{e}".red unless e.to_s.include?('uninitialized constant') end puts "Checking #{pkgName} for compatibility.".orange if @opt_verbose # If compatibility property does not exist, check if a binary package # exists, and if not, see if at least a source url exists. @compatibility = true @binary_url = '' @url = '' if @pkg.compatibility.nil? @binary_url = @pkg.get_binary_url(@device[:architecture]) @url = @pkg.get_url(@device[:architecture]) if !@binary_url puts "#{pkgName} is missing compatibility information".red #puts "url: #{@url}".green # If no source package is available, then package is not compatible. @compatibility = false unless @url puts "#{pkgName} compatibility is #{@compatibility}" if @opt_verbose end end if ( @pkg.compatibility&.include? 'all' or @pkg.compatibility&.include? ARCH or @pkg.compatibility.nil? ) and @compatibility #add to compatible packages puts "Adding #{pkgName} to compatible packages.".lightgreen if @opt_verbose @device[:compatible_packages].push(name: @pkg.name) else puts "#{pkgName} is not a compatible package.".lightred if @opt_verbose end end File.open(CREW_CONFIG_PATH + 'device.json', 'w') do |file| output = JSON.parse @device.to_json file.write JSON.pretty_generate(output) end puts 'Generating compatible packages done.'.orange if @opt_verbose end def search (pkgName, silent = false) pkgPath = CREW_PACKAGES_PATH + pkgName + '.rb' begin return set_package(pkgName, pkgPath) if File.exist?(pkgPath) rescue => e puts "Error with #{pkgName}.rb: #{e}".red unless e.to_s.include?('uninitialized constant') end abort "Package #{pkgName} not found. :(".lightred unless silent end def regexp_search(pkgPat) re = Regexp.new(pkgPat, true) results = Dir[CREW_PACKAGES_PATH + '*.rb'].sort \ .select { |f| File.basename(f, '.rb') =~ re } \ .each { |f| print_package(f, @opt_verbose) } if results.empty? Dir[CREW_PACKAGES_PATH + '*.rb'].each do |packagePath| packageName = File.basename packagePath, '.rb' begin set_package packageName, packagePath rescue => e puts "Error with #{pkgName}.rb: #{e}".red unless e.to_s.include?('uninitialized constant') end if ( @pkg.description =~ /#{pkgPat}/i ) print_current_package @opt_verbose results.push(packageName) end end end abort "Package #{pkgPat} not found. :(".lightred if results.empty? end def help(pkgName) case pkgName when "build" puts "Build package(s)." puts "Usage: crew build [-k|--keep] [-v|--verbose] [ ...]" puts "Build package(s) from source and place the archive and checksum in the current working directory." puts "If `-k` or `--keep` is present, the `CREW_BREW_DIR` (#{CREW_BREW_DIR}) directory will remain." puts "If `-v` or `--verbose` is present, extra information will be displayed." when "const" puts "Display constant(s)." puts "Usage: crew const [ ...]" puts "If no constants are provided, all constants will be displayed." when "deps" puts "Display dependencies of package(s)." puts "Usage: crew deps [ ...]" when "download" puts "Download package(s)." puts "Usage: crew download [-v|--verbose] [ ...]" puts "Download package(s) to `CREW_BREW_DIR` (#{CREW_BREW_DIR}), but don't install." puts "If `-v` or `--verbose` is present, extra information will be displayed." when "files" puts "Display installed files of package(s)." puts "Usage: crew files [ ...]" puts "The package(s) must be currently installed." when "install" puts "Install package(s)." puts "Usage: crew install [-k|--keep] [-s|--build-from-source] [-S|--recursive-build] [-v|--verbose] [ ...]" puts "The package(s) must have a valid name. Use `crew search ` to search for packages to install." puts "If `-k` or `--keep` is present, the `CREW_BREW_DIR` (#{CREW_BREW_DIR}) directory will remain." puts "If `-s` or `--build-from-source` is present, the package(s) will be compiled instead of installed via binary." puts "If `-S` or `--recursive-build` is present, the package(s), including all dependencies, will be compiled instead of installed via binary." puts "If `-v` or `--verbose` is present, extra information will be displayed." when "list" puts "List packages" puts "Usage: crew list [-v|--verbose] available|installed|compatible|incompatible" when "postinstall" puts "Display postinstall messages of package(s)." puts "Usage: crew postinstall [ ...]" puts "The package(s) must be currently installed." when "reinstall" puts "Remove and install package(s)." puts "Usage: crew reinstall [-k|--keep] [-s|--build-from-source] [-S|--recursive-build] [-v|--verbose] [ ...]" puts "If `-k` or `--keep` is present, the `CREW_BREW_DIR` (#{CREW_BREW_DIR}) directory will remain." puts "If `-s` or `--build-from-source` is present, the package(s) will be compiled instead of installed via binary." puts "If `-S` or `--recursive-build` is present, the package(s), including all dependencies, will be compiled instead of installed via binary." puts "If `-v` or `--verbose` is present, extra information will be displayed." when "remove" puts "Remove package(s)." puts "Usage: crew remove [-v|--verbose] [ ...]" puts "The package(s) must be currently installed." puts "If `-v` or `--verbose` is present, extra information will be displayed." when "search" puts "Look for package(s)." puts "Usage: crew search [-v|--verbose] [ ...]" puts "If is omitted, all packages will be returned." puts "If the package color is " + "green".lightgreen + ", it means the package is installed." puts "If the package color is " + "red".lightred + ", it means the architecture is not supported." puts "The string can also contain regular expressions." puts "If `-v` or `--verbose` is present, homepage, version and license will be displayed." puts "Examples:" puts " crew search ^lib".lightblue + " will display all packages that start with `lib`." puts " crew search audio".lightblue + " will display all packages with `audio` in the name." puts " crew search | grep -i audio".lightblue + " will display all packages with `audio` in the name or description." puts " crew search git -v".lightblue + " will display packages with `git` in the name along with homepage, version and license." when "update" puts "Update crew." puts "Usage: crew update" puts "This only updates crew itself. Use `crew upgrade` to update packages." puts "Usage: crew update compatible" puts "This updates the crew package compatibility list." when "upgrade" puts "Update package(s)." puts "Usage: crew upgrade [-v|--verbose] [-s|--build-from-source] [ ...]" puts "If package(s) are omitted, all packages will be updated. Otherwise, specific package(s) will be updated." puts "Use `crew update` to update crew itself." puts "If `-s` or `--build-from-source` is present, the package(s) will be compiled instead of upgraded via binary." puts "If `-v` or `--verbose` is present, extra information will be displayed." when "whatprovides" puts "Determine which package(s) contains file(s)." puts "Usage: crew whatprovides ..." puts "The is a search string which can contain regular expressions." else puts "Available commands: #{@cmds.join(', ')}" end end def const(var) if var value = eval(var) puts "#{var}=#{value}" else @ruby_default_constants = %w[ ARGF ARGV CROSS_COMPILING DOC ENV GC IO JSON OpenSSL Q R RUBY_COPYRIGHT RUBY_DESCRIPTION RUBY_ENGINE RUBY_ENGINE_VERSION RUBYGEMS_ACTIVATION_MONITOR RUBY_PATCHLEVEL RUBY_PLATFORM RUBY_RELEASE_DATE RUBY_REVISION RUBY_VERSION RubyVM S STDERR STDIN STDOUT StringIO TOPLEVEL_BINDING URI ] # Get all constants @constants = Module.constants.select {|e| e =~ /[[:upper:]]$/} # Reject all constants which match the default list @constants = @constants.map(&:to_s).reject{ |e| @ruby_default_constants.find{ |f| /\A#{e}\z/ =~ f }} # Print a sorted list of the remaining constants used by crew. @constants.sort.each { |var| value = eval(var.to_s) puts "#{var}=#{value}" } end end def human_size (bytes) kilobyte = 1024.0 megabyte = kilobyte * kilobyte gigabyte = megabyte * kilobyte if bytes < kilobyte units = 'B' size = bytes end if bytes >= kilobyte and bytes < megabyte units = 'KB' size = bytes / kilobyte end if bytes >= megabyte and bytes < gigabyte units = 'MB' size = bytes / megabyte end if bytes >= gigabyte units = 'GB' size = bytes / gigabyte end return sprintf('%.2f', size.to_s) + units end def files (pkgName) filelist = "#{CREW_META_PATH}#{pkgName}.filelist" if File.exist? filelist system "sort #{filelist}" lines = File.readlines(filelist).size size = 0 File.readlines(filelist).each do |filename| size += File.size(filename.chomp) if File.exist? filename.chomp end humansize = human_size(size) puts "Total found: #{lines}".lightgreen puts "Disk usage: #{humansize}".lightgreen else puts "Package #{pkgName} is not installed. :(".lightred end end def whatprovides (regexPat) # Use grep version command to ascertain whether we have a working grep. unless system('grep -V > /dev/null 2>&1') abort 'Grep is not working. Please install it with \'crew install grep\''.lightred end fileArray = [] @grepresults = %x[grep "#{regexPat}" #{CREW_META_PATH}*.filelist].chomp.gsub('.filelist','').gsub(':',': ').gsub(CREW_META_PATH,'').split(/$/).map(&:strip) @grepresults.each { |fileLine| fileArray.push(fileLine) } unless fileArray.empty? fileArray.sort.each do |item| puts item end puts "\nTotal found: #{fileArray.length}".lightgreen end end def update abort "'crew update' is used to update crew itself. Use 'crew upgrade [ ...]' to update specific packages.".orange if @pkgName @crew_testing_repo = ENV['CREW_TESTING_REPO'] @crew_testing_branch = ENV['CREW_TESTING_BRANCH'] @crew_testing = ENV['CREW_TESTING'] @crew_testing = 0 if @crew_testing_repo.nil? || @crew_testing_repo.empty? @crew_testing = 0 if @crew_testing_branch.nil? || @crew_testing_branch.empty? #update package lists Dir.chdir CREW_LIB_PATH do if @crew_testing == '1' puts "Updating crew from testing repository..." system "git remote add testing #{@crew_testing_repo} 2>/dev/null || \ git remote set-url testing #{@crew_testing_repo}" system "git fetch testing #{@crew_testing_branch}" system "git reset --hard testing/#{@crew_testing_branch}" else system 'git fetch origin master' system 'git reset --hard origin/master' end end puts 'Package lists, crew, and library updated.' #update compatible packages generate_compatible #check for outdated installed packages puts 'Checking for package updates...' canBeUpdated = 0 @device[:installed_packages].each do |package| search package[:name], true if package[:version] != @pkg.version canBeUpdated += 1 puts @pkg.name + ' could be updated from ' + package[:version] + ' to ' + @pkg.version end end if canBeUpdated > 0 puts puts "Run `crew upgrade` to update all packages or `crew upgrade [ ...]` to update specific packages." else puts "Your software is up to date.".lightgreen end end def upgrade if @pkgName currentVersion = nil @device[:installed_packages].each do |package| if package[:name] == @pkg.name currentVersion = package[:version] end end if currentVersion != @pkg.version puts "Updating #{@pkg.name}..." @pkg.in_upgrade = true resolve_dependencies_and_install @pkg.in_upgrade = false else puts "#{@pkg.name} is already up to date.".lightgreen end else # Make an installed packages list belong to the dependency order dependencies = [] @device[:installed_packages].each do |package| # skip package if it is dependent other packages previously checked next if dependencies.include? package[:name] # add package itself dependencies = [ package[:name] ].concat(dependencies) # expand dependencies and add it to the dependencies list search package[:name], true @dependencies = [] exp_dep = expand_dependencies dependencies = exp_dep.concat(dependencies) end dependencies.uniq! # Check version number of installed package and make a target list toBeUpdated = [] dependencies.each do |dep| package = @device[:installed_packages].find {|pkg| pkg[:name] == dep} next unless package search package[:name], true if package[:version] != @pkg.version toBeUpdated.push(package[:name]) end end # Adjust package install ordering for upgrades. CREW_FIRST_PACKAGES.each do |pkg| # Add package to beginning. toBeUpdated.insert(0,toBeUpdated.delete(pkg)) if toBeUpdated.include? pkg end CREW_LAST_PACKAGES.each do |pkg| if toBeUpdated.include? pkg # Add package to beginning. toBeUpdated.insert(0,toBeUpdated.delete(pkg)) # Now rotate first package to last package. toBeUpdated=toBeUpdated.rotate(1) end end unless toBeUpdated.empty? puts 'Updating packages...' toBeUpdated.each do |package| search package print_current_package puts "Updating " + @pkg.name + "..." if @opt_verbose @pkg.in_upgrade = true resolve_dependencies_and_install @pkg.in_upgrade = false end puts "Packages have been updated.".lightgreen else puts "Your software is already up to date.".lightgreen end end end def download url = @pkg.get_url(@device[:architecture]) source = @pkg.is_source?(@device[:architecture]) uri = URI.parse url filename = File.basename(uri.path) sha256sum = @pkg.get_sha256(@device[:architecture]) @extract_dir = @pkg.get_extract_dir if !url abort "No precompiled binary or source is available for #{@device[:architecture]}.".lightred elsif !source puts "Precompiled binary available, downloading..." elsif @pkg.build_from_source puts "Downloading source..." elsif uri =~ /^SKIP$/i puts "Skipping source download..." else puts "No precompiled binary available for your platform, downloading source..." end Dir.chdir CREW_BREW_DIR do case File.basename(filename) # Sources that download with curl when /\.zip$/i, /\.(tar(\.(gz|bz2|xz|lz))?|tgz|tbz|tpxz|txz)$/i, /\.deb$/i, /\.AppImage$/i # Recall file from cache if requested if CREW_CACHE_ENABLED puts "Looking for #{@pkg.name} archive in cache".orange if @opt_verbose cachefile = CREW_CACHE_DIR + filename if ! File.exist?(cachefile) puts 'Cannot find cached archive. 😔 Will download.'.lightred cachefile = '' else puts "#{@pkg.name.capitalize} archive file exists in cache".lightgreen if @opt_verbose if Digest::SHA256.hexdigest( File.read(cachefile) ) == sha256sum || sha256sum =~ /^SKIP$/i begin # Hard link cached file if possible. FileUtils.ln cachefile, CREW_BREW_DIR, force: true, verbose: @fileutils_verbose unless File.identical?(cachefile,CREW_BREW_DIR + '/' + filename) puts "Archive hard linked from cache".green if @opt_verbose rescue # Copy cached file if hard link fails. FileUtils.cp cachefile, CREW_BREW_DIR, verbose: @fileutils_verbose unless File.identical?(cachefile,CREW_BREW_DIR + '/' + filename) puts "Archive copied from cache".green if @opt_verbose end puts "Archive found in cache".lightgreen return {source: source, filename: filename} else puts 'Cached archive checksum mismatch. 😔 Will download.'.lightred cachefile = '' end end end # Download file if not cached. system "#{CURL} --retry 3 -#{@verbose}#LC - --insecure \'#{url}\' --output #{filename}" abort 'Checksum mismatch. 😔 Try again.'.lightred unless Digest::SHA256.hexdigest( File.read(filename) ) == sha256sum || sha256sum =~ /^SKIP$/i puts "#{@pkg.name.capitalize} archive downloaded.".lightgreen # Stow file in cache if requested (and if file is not from cache). if CREW_CACHE_ENABLED and cachefile.to_s.empty? begin # Hard link to cache if possible. FileUtils.ln filename, CREW_CACHE_DIR, verbose: @fileutils_verbose puts "Archive hard linked to cache".green if @opt_verbose rescue # Copy to cache if hard link fails. FileUtils.cp filename, CREW_CACHE_DIR, verbose: @fileutils_verbose puts "Archive copied to cache".green if @opt_verbose end puts 'Archive copied to cache.'.lightgreen end return {source: source, filename: filename} # Sources that download with git when /\.git$/i # Recall repository from cache if requested if CREW_CACHE_ENABLED if @pkg.git_branch.nil? || @pkg.git_branch.empty? cachefile = CREW_CACHE_DIR + filename + @pkg.git_hashtag + '.tar.xz' puts "cachefile is #{cachefile}".orange if @opt_verbose else # Use to the day granularity for a branch timestamp. cachefile = CREW_CACHE_DIR + filename + @pkg.git_branch.gsub(/[^0-9A-Za-z.\-]/, '_') + Time.now.strftime("%m%d%Y") + '.tar.xz' puts "cachefile is #{cachefile}".orange if @opt_verbose end if File.file?(cachefile) if system "cd #{CREW_CACHE_DIR} && sha256sum -c #{cachefile}.sha256" FileUtils.mkdir @extract_dir system "tar x#{@verbose}f #{cachefile} -C #{@extract_dir}" return {source: source, filename: filename} else puts 'Cached git repository checksum mismatch. 😔 Will download.'.lightred end else puts 'Cannot find cached git repository. 😔 Will download.'.lightred end end # Download via git Dir.mkdir @extract_dir Dir.chdir @extract_dir do system 'git init' system 'git config advice.detachedHead false' system 'git config init.defaultBranch master' system "git remote add origin #{@pkg.source_url}", exception: true unless @pkg.git_branch.nil? || @pkg.git_branch.empty? # Leave a message because this step can be slow. puts "Downloading src from a git branch. This may take a while..." system "git remote set-branches origin #{@pkg.git_branch}", exception: true system "git fetch --progress origin #{@pkg.git_branch}", exception: true system "git checkout #{@pkg.git_hashtag}", exception: true else system "git fetch --depth 1 origin #{@pkg.git_hashtag}", exception: true system 'git checkout FETCH_HEAD' end system 'git submodule update --init --recursive' puts 'Repository downloaded.'.lightgreen end # Stow file in cache if requested if CREW_CACHE_ENABLED puts 'Caching downloaded git repo...' Dir.chdir "#{@extract_dir}" do system "tar c#{@verbose}Jf #{cachefile} \ $(find -mindepth 1 -maxdepth 1 -printf '%P\n')" end system "sha256sum #{cachefile} > #{cachefile}.sha256" puts 'Git repo cached.'.lightgreen end when /^SKIP$/i Dir.mkdir @extract_dir end end return {source: source, filename: filename} end def unpack(meta) target_dir = nil Dir.chdir CREW_BREW_DIR do FileUtils.mkdir_p @extract_dir, verbose: @fileutils_verbose case File.basename meta[:filename] when /\.zip$/i puts "Unpacking archive using 'unzip', this may take a while..." _verbopt = @opt_verbose ? '-v' : '-qq' system 'unzip', _verbopt, '-d', @extract_dir, meta[:filename], exception: true when /\.(tar(\.(gz|bz2|xz|lz))?|tgz|tbz|txz)$/i puts "Unpacking archive using 'tar', this may take a while..." system "tar x#{@verbose}f #{meta[:filename]} -C #{@extract_dir}", exception: true when /\.deb$/i puts "Unpacking archive using 'ar', this may take a while..." suffix = `ar -t #{meta[:filename]}`.scan(/data.(.*)/)[0][0] case suffix when 'tar.xz' tar_opt = '--xz' when 'tgz', 'tar.gz' tar_opt = '--gzip' when 'bzip2' tar_opt = '--bzip2' end system "ar -p #{meta[:filename]} data.#{suffix} | tar x#{@verbose} #{tar_opt} -C #{@extract_dir}", exception: true when /\.AppImage$/i puts "Unpacking 'AppImage' archive, this may take a while..." FileUtils.chmod 0o755, meta[:filename], verbose: @fileutils_verbose Dir.chdir @extract_dir do system "../#{meta[:filename]} --appimage-extract", exception: true end when /\.tpxz$/i unless File.exist?("#{CREW_PREFIX}/bin/pixz") abort 'Pixz is needed for this install. Please install it with \'crew install pixz\''.lightred end puts "Unpacking 'tpxz' archive using 'tar', this may take a while..." system "tar -Ipixz -x#{@verbose}f #{meta[:filename]} -C #{@extract_dir}", exception: true end if meta[:source] == true # Check the number of directories in the archive entries = Dir["#{@extract_dir}/*"] entries = Dir[@extract_dir] if entries.empty? if entries.empty? abort "Empty archive: #{meta[:filename]}".lightred elsif entries.length == 1 && File.directory?(entries.first) # Use `extract_dir/dir_in_archive` if there is only one directory. target_dir = entries.first else # Use `extract_dir` otherwise target_dir = @extract_dir end else # Use `extract_dir` for binary distribution target_dir = @extract_dir end end return CREW_BREW_DIR + target_dir end def build_and_preconfigure(target_dir) Dir.chdir target_dir do puts 'Building from source, this may take a while...' # Rename *.la files temporarily to *.la_tmp to avoid # libtool: link: '*.la' is not a valid libtool archive. # See https://gnunet.org/faq-la-files and # https://stackoverflow.com/questions/42963653/libquadmath-la-is-not-a-valid-libtool-archive-when-configuring-openmpi-with-g puts 'Rename all *.la files to *.la_tmp'.lightblue system "find #{CREW_LIB_PREFIX} -type f -name *.la -print0 | xargs --null -I{} mv #{@short_verbose} {} {}_tmp" @pkg.in_build = true @pkg.patch @pkg.prebuild @pkg.build @pkg.in_build = false # wipe crew destdir FileUtils.rm_rf Dir.glob("#{CREW_DEST_DIR}/*"), verbose: @fileutils_verbose puts 'Preconfiguring package...' @pkg.install # Rename all *.la_tmp back to *.la to avoid # cannot access '*.la': No such file or directory puts 'Rename all *.la_tmp files back to *.la'.lightblue system "find #{CREW_LIB_PREFIX} -type f -name '*.la_tmp' -exec sh -c 'mv #{@short_verbose} \"$1\" \"${1%.la_tmp}.la\"' _ {} \\;" end end def pre_flight puts 'Performing pre-flight checks...' @pkg.preflight end def pre_install(dest_dir) Dir.chdir dest_dir do puts 'Performing pre-install...' @pkg.preinstall end end def post_install Dir.mktmpdir do |post_install_tempdir| Dir.chdir post_install_tempdir do puts "Performing post-install for #{@pkg.name}...".lightblue @pkg.postinstall end end end def compress_doc(dir) # check whether crew should compress return if CREW_NOT_COMPRESS || ENV['CREW_NOT_COMPRESS'] || !File.exist?("#{CREW_PREFIX}/bin/compressdoc") if Dir.exist? dir system "find #{dir} -type f ! -perm -200 | xargs -r chmod u+w" system "compressdoc --gzip -9 #{@short_verbose} #{dir}" end end def prepare_package(destdir) Dir.chdir destdir do # Avoid /usr/local/share/info/dir{.gz} file conflict: # The install-info program maintains a directory of installed # info documents in /usr/share/info/dir for the use of info # readers. This file must not be included in packages other # than install-info. # https://www.debian.org/doc/debian-policy/ch-docs.html#info-documents FileUtils.rm "#{CREW_DEST_PREFIX}/share/info/dir" if File.exist?("#{CREW_DEST_PREFIX}/share/info/dir") # Remove all perl module files which will conflict if @pkg.name =~ /^perl_/ puts "Removing .packlist and perllocal.pod files to avoid conflicts with other perl packages.".orange system "find #{CREW_DEST_DIR} -type f \\( -name '.packlist' -o -name perllocal.pod \\) -delete" end # compress manual files compress_doc "#{CREW_DEST_PREFIX}/man" compress_doc "#{CREW_DEST_PREFIX}/info" compress_doc "#{CREW_DEST_PREFIX}/share/man" compress_doc "#{CREW_DEST_PREFIX}/share/info" # create file list system 'find . -type f > ../filelist' system 'find . -type l >> ../filelist' system 'cut -c2- ../filelist > filelist' # check for conflicts with other installed files puts "Checking for conflicts with files from installed packages..." conflicts = [] @conflictscmd = "grep --exclude #{CREW_META_PATH}#{@pkg.name}.filelist \ -Fxf filelist #{CREW_META_PATH}*.filelist | tr ':' ' ' | \ sed 's,.filelist,,g' | sed 's,#{CREW_META_PATH},,g'" conflicts << %x[#{@conflictscmd}].chomp.split(" ") conflicts.reject!(&:empty?) unless conflicts.empty? puts "Unable to complete this build since there is a conflict with the same file in another package.".lightred pp = '' conflicts.each do |p, f| puts "\n#{p} package conflicts below:".lightred if p != pp puts f.lightred pp = p end abort unless ENV['CREW_CONFLICTS_ONLY_ADVISORY'] == '1' end # create directory list system 'find . -type d > ../dlist' system 'cut -c2- ../dlist > dlistcut' system 'tail -n +2 dlistcut > dlist' # remove temporary files FileUtils.rm_rf ['dlistcut', '../dlist', '../filelist'], verbose: @fileutils_verbose strip_dir destdir # make hard linked files symlinks and use upx on executables shrink_dir destdir end end def strip_find_files(find_cmd, strip_option = "") # check whether crew should strip return if CREW_NOT_STRIP || ENV['CREW_NOT_STRIP'] || !File.exist?("#{CREW_PREFIX}/bin/llvm-strip") # run find_cmd and strip only ar or ELF files system "#{find_cmd} | xargs -r chmod u+w" system "#{find_cmd} | xargs -r sh -c 'for i in \"$0\" \"$@\"; do case \"$(head -c 4 $i)\" in ?ELF|\!?ar) echo \"$i\";; esac ; done' | xargs -r llvm-strip #{strip_option}" end def strip_dir(dir) unless CREW_NOT_STRIP || ENV['CREW_NOT_STRIP'] Dir.chdir dir do # Strip libraries with -S puts "Stripping libraries..." strip_find_files "find . -type f \\( -name 'lib*.a' -o -name 'lib*.so*' \\) -print", "-S" # Strip binaries but not compressed archives puts "Stripping binaries..." extensions = [ 'bz2', 'gz', 'lha', 'lz', 'lzh', 'rar', 'tar', 'tbz', 'tgz', 'tpxz', 'txz', 'xz', 'Z', 'zip' ] inames = extensions.join(' ! -iname *\.') strip_find_files "find . -type f ! -iname *\.#{inames} -perm /111 -print | sed -e '/lib.*\.a$/d' -e '/lib.*\.so/d'" end end end def shrink_dir(dir) # We might also want a package option to avoid using these tools # on specific packages such as sommelier & xwayland. if ENV['CREW_SHRINK_ARCHIVE'] == '1' Dir.chdir dir do if File.exist?("#{CREW_PREFIX}/bin/rdfind") puts "Using rdfind to find duplicate or hard linked files." system "#{CREW_PREFIX}/bin/rdfind -removeidentinode true -makesymlinks true -makeresultsfile false ." end if File.exist?("#{CREW_PREFIX}/bin/symlinks") puts "Using symlinks tool to make absolute symlinks relative" system 'symlinks -cr .' if File.exist?("#{CREW_PREFIX}/bin/symlinks") end # Issues with non-x86_64 in compressing libraries, so just compress # non-libraries. Also note that one needs to use "upx -d" on a # compressed file to use ldd. # sommelier also isn't happy when sommelier and xwayland are compressed # so don't compress those packages. if File.exist?("#{CREW_PREFIX}/bin/upx") # Logic here is to find executable binaries, compress after making # a backup, then expand the compressed file with upx. If the # expansion doesn't error out then it is ok to delete the backup. @execfiles = %x[find . -executable -type f ! \\( -name \"*.so*\" -o -name \"*.a\" \\) -exec sh -c \"file -i \'{}\' | grep -q \'executable; charset=binary\'\" \\; -print].chomp unless @execfiles.empty? or @execfiles.nil? puts "Using upx to shrink binaries." @execfiles.each_line do |execfile| system "upx --best -k --overlay=skip #{execfile} && \ \( upx -t #{execfile} && rm #{execfile}.~ || mv #{execfile}.~ #{execfile}\)" end end end end end end def install_package(pkgdir) Dir.chdir pkgdir do # install filelist, dlist and binary files puts 'Performing install...' FileUtils.mv 'dlist', CREW_META_PATH + @pkg.name + '.directorylist', verbose: @fileutils_verbose FileUtils.mv 'filelist', CREW_META_PATH + @pkg.name + '.filelist', verbose: @fileutils_verbose if Dir.exists? "#{pkgdir}/home" then system "tar -c#{@verbose}f - ./usr/* ./home/* | (cd /; tar xp --keep-directory-symlink -f -)" elsif Dir.exists? "#{pkgdir}/usr" then system "tar -c#{@verbose}f - ./usr/* | (cd /; tar xp --keep-directory-symlink -f -)" end end end def resolve_dependencies_and_install @resolve_dependencies_and_install = 1 unless @pkg.is_fake? # Process preflight block to see if package should even # be downloaded or installed. pre_flight end begin origin = @pkg.name @to_postinstall = [] resolve_dependencies search origin, true install @to_postinstall.append(@pkg.name) @to_postinstall.each do |dep| search dep post_install end rescue InstallError => e abort "#{@pkg.name} failed to install: #{e.to_s}".lightred ensure # cleanup unless @opt_keep FileUtils.rm_rf Dir.glob("#{CREW_BREW_DIR}/*") FileUtils.mkdir_p "#{CREW_BREW_DIR}/dest" # this is a little ugly, feel free to find a better way end end puts "#{@pkg.name.capitalize} installed!".lightgreen @resolve_dependencies_and_install = 0 end def expand_dependencies def push_dependencies if @pkg.is_binary?(@device[:architecture]) || (!@pkg.in_upgrade && !@pkg.build_from_source && @device[:installed_packages].any? { |pkg| pkg[:name] == @pkg.name }) # retrieve name of dependencies that doesn't contain :build tag check_deps = @pkg.dependencies.select {|k, v| !v.include?(:build)}.map {|k, v| k} else # retrieve name of all dependencies check_deps = @pkg.dependencies.map {|k, v| k} end # check all dependencies recursively check_deps.each do |dep| # build unique dependencies list unless @dependencies&.include?(dep) || dep == @pkgName @dependencies << dep search dep, true push_dependencies end end end push_dependencies end def resolve_dependencies abort "Package #{@pkg.name} is not compatible with your device architecture (#{ARCH}) :/".lightred unless @device[:compatible_packages].any? do |elem| elem[:name] == @pkg.name end @dependencies = [] if @pkg.build_from_source # make sure all buildessential packages are installed pkgname = @pkg.name search 'buildessential', true expand_dependencies search pkgname, true end expand_dependencies # leave only not installed packages in dependencies @dependencies.select! {|name| @device[:installed_packages].none? {|pkg| pkg[:name] == name}} return if @dependencies.empty? puts 'The following packages also need to be installed: ' deps = @dependencies # populate arrays with common elements begin_packages = deps & CREW_FIRST_PACKAGES end_packages = deps & CREW_LAST_PACKAGES @dependencies.each do |dep| depends = nil File.open("#{CREW_PACKAGES_PATH}#{dep}.rb") do |f| f.each_line do |line| found = line[/depends_on/] if line.ascii_only? if found depends = true break end end end # if a dependency package has no other dependencies, push to the front begin_packages.push dep unless depends end # Remove elements in another array deps -= begin_packages deps -= end_packages @dependencies = (begin_packages + deps + end_packages).uniq @dependencies.each do |dep| print dep + ' ' end puts print 'Do you agree? [Y/n] ' response = STDIN.getc case response when 'n' abort 'No changes made.' when "\n", "y", "Y" puts 'Proceeding...' proceed = true else puts "I don't understand `#{response}`. :(".lightred abort 'No changes made.' end if proceed @dependencies.each do |dep| search dep print_current_package install end if @resolve_dependencies_and_install == 1 or @resolve_dependencies_and_build == 1 @to_postinstall = @dependencies else @dependencies.each do |dep| search dep post_install end end end end def install if !@pkg.in_upgrade && @device[:installed_packages].any? { |pkg| pkg[:name] == @pkg.name } puts "Package #{@pkg.name} already installed, skipping...".lightgreen return end unless @pkg.is_fake? meta = download target_dir = unpack meta if meta[:source] == true # build from source and place binaries at CREW_DEST_DIR # CREW_DEST_DIR contains usr/local/... hierarchy build_and_preconfigure target_dir # prepare filelist and dlist at CREW_DEST_DIR prepare_package CREW_DEST_DIR # use CREW_DEST_DIR dest_dir = CREW_DEST_DIR else # use extracted binary directory dest_dir = target_dir end end # remove it just before the file copy if @pkg.in_upgrade puts 'Removing since upgrade or reinstall...' remove @pkg.name end unless @pkg.is_fake? # perform pre-install process pre_install dest_dir # perform install process install_package dest_dir unless @resolve_dependencies_and_install == 1 or @resolve_dependencies_and_build == 1 # perform post-install process post_install end end #add to installed packages @device[:installed_packages].push(name: @pkg.name, version: @pkg.version) File.open(CREW_CONFIG_PATH + 'device.json', 'w') do |file| output = JSON.parse @device.to_json file.write JSON.pretty_generate(output) end # Update shared library cache after install is complete. system "echo #{CREW_LIB_PREFIX} > #{CREW_PREFIX}/etc/ld.so.conf" system "#{CREW_PREFIX}/sbin/ldconfig -f #{CREW_PREFIX}/etc/ld.so.conf -C #{CREW_PREFIX}/etc/ld.so.cache" 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 post_install end search origin, true build_package Dir.pwd rescue InstallError => e abort "#{@pkg.name} failed to build: #{e.to_s}".lightred ensure #cleanup unless @opt_keep FileUtils.rm_rf Dir.glob("#{CREW_BREW_DIR}/*"), verbose: @fileutils_verbose FileUtils.mkdir_p CREW_BREW_DIR + '/dest', verbose: @fileutils_verbose #this is a little ugly, feel free to find a better way end end puts "#{@pkg.name} is built!".lightgreen @resolve_dependencies_and_build = 0 end def build_package(pwd) abort 'It is not possible to build a fake package'.lightred if @pkg.is_fake? abort 'It is not possible to build without source'.lightred if !@pkg.is_source?(@device[:architecture]) # download source codes and unpack it meta = download target_dir = unpack meta # build from source and place binaries at CREW_DEST_DIR build_and_preconfigure target_dir # call check method here. this check method is called by this function only, # therefore it is possible place time consuming tests in the check method. if Dir.exist? target_dir Dir.chdir target_dir do puts 'Checking...' @pkg.check end end # prepare filelist and dlist at CREW_DEST_DIR prepare_package CREW_DEST_DIR # build package from filelist, dlist and binary files in CREW_DEST_DIR puts 'Archiving...' archive_package pwd end def archive_package(pwd) unless ENV['CREW_USE_PIXZ'] == '0' || !File.exist?("#{CREW_PREFIX}/bin/pixz") puts "Using pixz to compress archive." pkg_name = "#{@pkg.name}-#{@pkg.version}-chromeos-#{@device[:architecture]}.tpxz" Dir.chdir CREW_DEST_DIR do # Use smaller blocks with "-f0.25" to make random access faster. system "tar c#{@verbose} * | pixz -f0.25 -9 > #{pwd}/#{pkg_name}" end else pkg_name = "#{@pkg.name}-#{@pkg.version}-chromeos-#{@device[:architecture]}.tar.xz" Dir.chdir CREW_DEST_DIR do system "tar c#{@verbose}Jf #{pwd}/#{pkg_name} *" end end system "sha256sum #{pwd}/#{pkg_name} > #{pwd}/#{pkg_name}.sha256" end def remove(pkgName) #make sure the package is actually installed unless @device[:installed_packages].any? { |pkg| pkg[:name] == pkgName } || File.exist?("#{CREW_META_PATH}#{pkgName}.filelist") puts "Package #{pkgName} isn't installed.".lightred return end #if the filelist exists, remove the files and directories installed by the package if File.file?("#{CREW_META_PATH}#{pkgName}.filelist") Dir.chdir CREW_CONFIG_PATH do #remove all files installed by the package File.open("meta/#{pkgName}.filelist").each_line do |line| begin puts 'Removing file ' + line.chomp + ''.lightred if @opt_verbose File.unlink line.chomp rescue => exception #swallow exception end end #remove all directories installed by the package File.readlines("meta/#{pkgName}.directorylist").reverse.each do |line| begin puts 'Removing directory ' + line.chomp + ''.lightred if @opt_verbose Dir.rmdir line.chomp rescue => exception #swallow exception end end #remove the file and directory list File.unlink "meta/#{pkgName}.filelist" File.unlink "meta/#{pkgName}.directorylist" end end #remove from installed packages puts 'Removing package ' + pkgName + "".lightred if @opt_verbose @device[:installed_packages].each do |elem| @device[:installed_packages].delete elem if elem[:name] == pkgName end #update the device manifest File.open(CREW_CONFIG_PATH + 'device.json', 'w') do |file| out = JSON.parse @device.to_json file.write JSON.pretty_generate(out) end search pkgName, true @pkg.remove puts "#{pkgName.capitalize} removed!".lightgreen end def build_command(args) args[""].each do |name| @pkgName = name search @pkgName print_current_package @opt_verbose resolve_dependencies_and_build end end def const_command(args) unless args[""].empty? args[""].each do |name| const name end else const nil end end def deps_command(args) args[""].each do |name| @dependencies = [] @pkgName = name search @pkgName print_current_package expand_dependencies puts @dependencies end end def download_command(args) args[""].each do |name| @pkgName = name search @pkgName print_current_package @opt_verbose download end end def files_command(args) args[""].each do |name| @pkgName = name search @pkgName print_current_package files name end end def help_command(args) if args[""] help args[""] else puts "Usage: crew help " help nil end end def install_command(args) args[""].each do |name| @pkgName = name search @pkgName print_current_package true @pkg.build_from_source = true if @opt_src or @opt_recursive resolve_dependencies_and_install end end def list_command(args) if args['available'] list_available elsif args['installed'] puts list_installed elsif args['compatible'] list_compatible true elsif args['incompatible'] list_compatible false end end def postinstall_command(args) args[""].each do |name| @pkgName = name search @pkgName, true if @device[:installed_packages].any? do |elem| elem[:name] == @pkgName end @pkg.postinstall else puts "Package #{@pkgName} is not installed. :(".lightred end end end def reinstall_command(args) args[""].each do |name| @pkgName = name search @pkgName print_current_package @pkg.build_from_source = true if @opt_src or @opt_recursive if @pkgName @pkg.in_upgrade = true resolve_dependencies_and_install @pkg.in_upgrade = false end end end def remove_command(args) args[""].each do |name| remove name end end def search_command(args) args[""].each do |name| regexp_search name end.empty? and begin list_packages end end def update_command(args) if args[''] generate_compatible else update end end def upgrade_command(args) args[""].each do |name| @pkgName = name search @pkgName print_current_package @pkg.build_from_source = true if @opt_src upgrade end.empty? and begin upgrade end end def whatprovides_command(args) args[""].each do |name| whatprovides name end end def is_command(name) return false if name =~ /^[-<]/ return true end command_name = args.find { |k, v| v && is_command(k) } [0] function = command_name + '_command' send(function, args)