#!/usr/bin/env ruby require 'find' require 'net/http' require 'uri' require 'digest/sha1' require 'json' require 'fileutils' @command = ARGV[0] @pkgName = ARGV[1] ARCH = `uname -m`.strip ARCH_LIB = if ARCH == 'x86_64' then 'lib64' else 'lib' end CREW_PREFIX = '/usr/local' CREW_LIB_PREFIX = CREW_PREFIX + '/' + ARCH_LIB CREW_LIB_PATH = CREW_PREFIX + '/lib/crew/' CREW_CONFIG_PATH = CREW_PREFIX + '/etc/crew/' CREW_BREW_DIR = CREW_PREFIX + '/tmp/crew/' CREW_DEST_DIR = CREW_BREW_DIR + 'dest' # Set CREW_NPROC from environment variable or `nproc` if ENV["CREW_NPROC"].to_s == '' CREW_NPROC = `nproc`.strip else CREW_NPROC = ENV["CREW_NPROC"] end # Set CREW_NOT_STRIP from environment variable CREW_NOT_STRIP = ENV["CREW_NOT_STRIP"] # Set XZ_OPT environment variable for build command. # If CREW_XZ_OPT is defined, use it by default. Use `-7e`, otherwise. if ENV["CREW_XZ_OPT"].to_s == '' ENV["XZ_OPT"] = "-7e" else ENV["XZ_OPT"] = ENV["CREW_XZ_OPT"] end $LOAD_PATH.unshift "#{CREW_LIB_PATH}lib" USER = `whoami`.chomp # colorization class String def colorize(color_code, shade) "\e[#{shade};#{color_code}m#{self}\e[0m" end def black colorize(30, 0) end def red colorize(31, 0) end def green colorize(32, 0) end def orange colorize(33, 0) end def blue colorize(34, 0) end def purple colorize(35, 0) end def cyan colorize(36, 0) end def lightgray colorize(37, 0) end def gray colorize(30, 1) end def lightred colorize(31, 1) end def lightgreen colorize(32, 1) end def yellow colorize(33, 1) end def lightblue colorize(34, 1) end def lightpurple colorize(35, 1) end def lightcyan colorize(36, 1) end def white colorize(37, 1) end end #disallow sudo abort "Chromebrew should not be run as root.".lightred if Process.uid == 0 @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(pkgName, extra = false) search pkgName, true Find.find(CREW_CONFIG_PATH + 'meta/') do |packageList| print '(i) '.lightgreen if packageList == CREW_CONFIG_PATH + 'meta/' + pkgName + '.filelist' end print @pkg.name print ": #{@pkg.description}" if @pkg.description if extra puts "" puts @pkg.homepage if @pkg.homepage print "version #{@pkg.version}" end puts "" end def set_package (pkgName, silent = false) require CREW_LIB_PATH + 'packages/' + pkgName @pkg = Object.const_get(pkgName.capitalize) @pkg.name = pkgName print_package(pkgName, true) unless silent end def list_packages Find.find (CREW_LIB_PATH + 'packages') do |filename| if File.extname(filename) == '.rb' print_package File.basename filename, '.rb' end end end def search (pkgName, silent = false) Find.find (CREW_LIB_PATH + 'packages') do |filename| return set_package(pkgName, silent) if filename == CREW_LIB_PATH + 'packages/' + pkgName + '.rb' end abort "Package #{pkgName} not found. :(".lightred end def regexp_search(pkgName) results = Dir["#{CREW_LIB_PATH}packages/*.rb"] \ .select { |f| File.basename(f, '.rb') =~ Regexp.new(pkgName, true) } \ .collect { |f| File.basename(f, '.rb') } \ .each { |f| print_package(f, ARGV[2] == "extra") } if results.empty? Find.find ("#{CREW_LIB_PATH}packages/") do |packageName| if File.file? packageName package = File.basename packageName, '.rb' search package, true if ( @pkg.description =~ /#{pkgName}/i ) print_package(package, ARGV[2] == "extra") results.push(package) end end end end abort "Package #{pkgName} not found. :(".lightred unless results.length > 0 end def help (pkgName) case pkgName when "build" puts "Build a package." puts "Usage: crew build [package]" puts "Build [package] from source and place the archive and checksum in the current working directory." when "download" puts "Download a package." puts "Usage: crew download [package]" puts "Download [package] to `CREW_BREW_DIR` (`/usr/local/tmp/crew` by default), but don't install it." when "install" puts "Install a package." puts "Usage: crew install [package]" puts "The [package] must be a valid name. Use `crew search [package]` to search for a package to install." when "remove" puts "Remove a package." puts "Usage: crew remove [package]" puts "The [package] must be currently installed." when "search" puts "Look for a package." puts "Usage: crew search [package] [extra]" puts "If [package] is omitted, all packages will be returned." puts "(i)".lightgreen + " in front of the name means the package is installed." puts "The [package] string can also contain regular expressions." puts "If the keyword 'extra' is appended, homepage and version will be displayed." puts "Examples:" puts " crew search | grep '(i)'".lightblue + " will display all installed packages." puts " crew search | grep -v '(i)'".lightblue + " will display all available packages not already installed." 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 description." puts " crew search git extra".lightblue + " will display the git package along with homepage and version." when "update" puts "Update crew." puts "Usage: crew update" puts "This only updates crew itself. Use 'crew upgrade' to update packages." when "upgrade" puts "Update package(s)." puts "Usage: crew upgrade [package]" puts "If [package] is omitted, all packages will be updated. Otherwise, a specific package will be updated." puts "Use 'crew update' to update crew itself." when "whatprovides" puts "Determine which package(s) contains file(s)." puts "Usage: crew whatprovides [pattern]" puts "The [pattern] is a search string which can contain regular expressions." else puts "Available commands: build, download, install, remove, search, update, upgrade, whatprovides" end end def whatprovides (pkgName) fileArray = [] Find.find (CREW_CONFIG_PATH + 'meta/') do |packageList| if File.file? packageList if packageList[/\.filelist$/] packageName = File.basename packageList, '.filelist' File.readlines(packageList).each do |line| found = line[/#{Regexp.new(pkgName)}/] if found fileLine = packageName + ': ' + line if not fileArray.include? fileLine fileArray.push(fileLine) end end end end end end if not 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 upgrade a specific package.".orange if @pkgName #update package lists Dir.chdir CREW_LIB_PATH do system "git fetch origin master" system "git reset --hard origin/master" end puts "Package lists, crew, and library updated." #check for outdated installed packages puts "Checking for package updates..." puts "" 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 upgrade everything or 'crew upgrade ' to upgrade a specific package." else puts "Your software is up to date.".lightgreen end end def upgrade if @pkgName search @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 a 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 depencencies and add it to the dependencies list search package[:name], true 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 if toBeUpdated.length > 0 puts "Updating packages..." toBeUpdated.each do |package| search package @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]) if !url abort "No precompiled binary for #{@device[:architecture]} nor source is available.".lightred elsif !source puts "Precompiled binary available, downloading..." elsif @pkg.build_from_source puts "Downloading source..." else puts "No precompiled binary available for your platform, downloading source..." end uri = URI.parse url filename = File.basename(uri.path) if source sha1sum = @pkg.source_sha1 sha256sum = @pkg.source_sha256 else sha1sum = @pkg.binary_sha1[@device[:architecture]] sha256sum = @pkg.binary_sha256[@device[:architecture]] end Dir.chdir CREW_BREW_DIR do system('wget', '--continue', '--no-check-certificate', url, '-O', filename) abort 'Checksum mismatch. :/ Try again.'.lightred unless Digest::SHA1.hexdigest( File.read("./#{filename}") ) == sha1sum or Digest::SHA256.hexdigest( File.read("./#{filename}") ) == sha256sum end puts "Archive downloaded".lightgreen return {source: source, filename: filename} end def unpack (meta) extract_dir = "#{meta[:filename]}.dir" target_dir = nil Dir.chdir CREW_BREW_DIR do puts "Unpacking archive, this may take a while..." Dir.mkdir("#{extract_dir}") unless Dir.exist?("#{extract_dir}") if meta[:filename][-4,4] == ".zip" system "unzip", "-qq", "-d", "#{extract_dir}", meta[:filename] else system "tar", "xf", meta[:filename], "-C", "#{extract_dir}" 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.length == 0 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..." @pkg.in_build = true @pkg.build @pkg.in_build = false system "rm -rf #{CREW_DEST_DIR}/*" #wipe crew destdir puts "Preconfiguring package..." @pkg.install end end def prepare_package (destdir) Dir.chdir destdir do #create directory list system "find . -type f > ../filelist" system "find . -type l >> ../filelist" system "cut -c2- ../filelist > filelist" #create file list system "find . -type d > ../dlist" system "cut -c2- ../dlist > dlistcut" system "tail -n +2 dlistcut > dlist" #remove temporary files system "rm dlistcut ../dlist ../filelist" end end def install_package (pkgdir) Dir.chdir pkgdir do FileUtils.mv 'dlist', CREW_CONFIG_PATH + "meta/#{@pkg.name}.directorylist" FileUtils.mv 'filelist', CREW_CONFIG_PATH + "meta/#{@pkg.name}.filelist" # Strip libraries with -S system "find . -name 'lib*.a' -print | xargs chmod u+w 2>/dev/null" unless CREW_NOT_STRIP system "find . -name 'lib*.a' -print | xargs strip -S 2>/dev/null" unless CREW_NOT_STRIP system "find . -name 'lib*.so*' -print | xargs chmod u+w 2>/dev/null" unless CREW_NOT_STRIP system "find . -name 'lib*.so*' -print | xargs strip -S 2>/dev/null" unless CREW_NOT_STRIP # Strip binaries system "find . -type f -perm /111 -print | sed -e '/lib.*\.a$/d' -e '/lib.*\.so/d' | xargs chmod u+w 2>/dev/null" unless CREW_NOT_STRIP system "find . -type f -perm /111 -print | sed -e '/lib.*\.a$/d' -e '/lib.*\.so/d' | xargs strip 2>/dev/null" unless CREW_NOT_STRIP system "tar cf - ./usr/* | (cd /; tar xp --keep-directory-symlink -f -)" end end def resolve_dependencies_and_install begin origin = @pkg.name resolve_dependencies search origin, true install rescue InstallError => e abort "#{@pkg.name} failed to install: #{e.to_s}".lightred ensure #cleanup unless ARGV[2] == 'keep' Dir.chdir CREW_BREW_DIR do system "rm -rf *" system "mkdir dest" #this is a little ugly, feel free to find a better way end end end end def expand_dependencies @dependencies = [] # check source packages existance @source_package = 0 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} elsif @pkg.is_fake? # retrieve name of all dependencies check_deps = @pkg.dependencies.map {|k, v| k} else # retrieve name of all dependencies check_deps = @pkg.dependencies.map {|k, v| k} # count the number of source packages to add buildessential into dependencies later @source_package += 1 end # remove a dependent package which is equal to the target check_deps.select! {|name| @pkgName != name} # add new dependencies at the beginning of array @dependencies = check_deps.clone.concat(@dependencies) # check all dependencies recursively check_deps.each do |dep| search dep, true push_dependencies end end push_dependencies # Add buildessential's dependencies if any of dependent # packages are made from source if @source_package > 0 search 'buildessential', true push_dependencies end @dependencies.uniq end def resolve_dependencies dependencies = 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: " 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 install 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 abort "You don't have a working C compiler. Run 'crew install buildessential' to get one and try again.".lightred unless system("gcc", "--version") # 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 # remove it just before the file copy if @pkg.in_upgrade puts "Removing since upgrade..." remove @pkg.name end # install filelist, dlist and binary files puts "Installing..." install_package dest_dir 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 puts "#{@pkg.name.capitalize} installed!".lightgreen end def resolve_dependencies_and_build begin origin = @pkg.name # mark current package as which is required to compile from source @pkg.build_from_source = true resolve_dependencies search origin, true build_package Dir.pwd rescue InstallError => e abort "#{@pkg.name} failed to build: #{e.to_s}".lightred ensure #cleanup unless ARGV[2] == 'keep' Dir.chdir CREW_BREW_DIR do system "rm -rf *" system "mkdir dest" #this is a little ugly, feel free to find a better way end end end end def build_package (pwd) abort "It is not possible to build 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) pkg_name = "#{@pkg.name}-#{@pkg.version}-chromeos-#{@device[:architecture]}.tar.xz" Dir.chdir CREW_DEST_DIR do system "tar cJf #{pwd}/#{pkg_name} *" end Dir.chdir pwd do system "sha1sum #{pkg_name} > #{pkg_name}.sha1" system "sha256sum #{pkg_name} > #{pkg_name}.sha256" end puts "#{pkg_name} is built!".lightgreen end def remove (pkgName) #make sure the package is actually installed unless @device[:installed_packages].any? { |pkg| pkg[:name] == pkgName } 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_CONFIG_PATH}meta/#{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 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 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 @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 puts "#{pkgName.capitalize} removed!".lightgreen end case @command when "help" if @pkgName help @pkgName else puts "Usage: crew help [command]" help nil end when "search" if @pkgName regexp_search @pkgName else list_packages end when "whatprovides" if @pkgName whatprovides @pkgName else help "whatprovides" end when "download" if @pkgName search @pkgName download else help "download" end when "update" update when "upgrade" upgrade when "install" if @pkgName search @pkgName resolve_dependencies_and_install else help "install" end when "build" if @pkgName search @pkgName resolve_dependencies_and_build else help "build" end when "remove" if @pkgName remove @pkgName else help "remove" end when nil puts "Chromebrew, version 0.4.3" puts "Usage: crew [command] [package]" help nil else puts "I have no idea how to do #{@command} :(".lightred help nil end