#!/usr/bin/env ruby require 'find' require 'net/http' require 'uri' require 'digest/sha1' require 'json' require 'fileutils' @command = ARGV[0] @pkgName = ARGV[1] CREW_PREFIX = '/usr/local' 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 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 ARCH = `uname -m`.strip $LOAD_PATH.unshift "#{CREW_LIB_PATH}lib" USER = `whoami`.chomp #disallow sudo abort "Chromebrew should not be run as root." if Process.uid == 0 && @command != "remove" @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 set_package (pkgName, silent = false) require CREW_LIB_PATH + 'packages/' + pkgName @pkg = Object.const_get(pkgName.capitalize) @pkg.name = pkgName puts "Found #{pkgName}, version #{@pkg.version}" unless silent end def list_packages Find.find (CREW_LIB_PATH + 'packages') do |filename| Find.find(CREW_CONFIG_PATH + 'meta/') do |packageList| packageName = File.basename filename, '.rb' print '(i) ' if packageList == CREW_CONFIG_PATH + 'meta/' + packageName + '.filelist' end puts File.basename filename, '.rb' if File.extname(filename) == '.rb' 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 :(" 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]" puts "If [package] is omitted, all packages will be returned. (i) in front of the name means the package is installed." puts "Tips:" puts " crew search | grep '(i)' will return all installed packages." puts " crew search | grep -v '(i)' will return all available packages not already installed." 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}" end end def update abort "'crew update' is used to update crew itself. Use 'crew upgrade to upgrade a specific package." 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." 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." end else toBeUpdated = [] @device[:installed_packages].each do |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." else puts "Your software is already up to date." 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." 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 else sha1sum = @pkg.binary_sha1[@device[:architecture]] end Dir.chdir CREW_BREW_DIR do system('wget', '--continue', '--no-check-certificate', url, '-O', filename) abort 'Checksum mismatch :/ try again' unless Digest::SHA1.hexdigest( File.read("./#{filename}") ) == sha1sum end puts "Archive downloaded" 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}/*"] if entries.length == 0 abort "empty archive: #{meta[:filename]}" 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" system "tar cf - ./usr/* | (cd /; tar xp --keep-directory-symlink -f -)" end end def resolve_dependencies_and_install begin origin = @pkg.name resolveDependencies search origin, true install rescue InstallError => e abort "#{@pkg.name} failed to install: #{e.to_s}" 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 resolveDependencies @dependencies = [] # check source packages existance @source_package = 0 def push_dependencies if @pkg.is_binary?(@device[:architecture]) || (!@pkg.in_upgrade && @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} # insert only not installed packages into dependencies @dependencies = @check_deps.select {|name| @device[:installed_packages].none? {|pkg| pkg[:name] == name}}.concat(@dependencies) # check all dependencies recursively @check_deps.each do |dep| search dep, true push_dependencies end end push_dependencies # Add buildessential and solve its dependencies if any of dependent # packages are made from source if @source_package > 0 search 'buildessential', true push_dependencies end return if @dependencies.empty? puts "Following packages also need to be installed: " @dependencies.uniq! @dependencies.each do |dep| print dep + " " end puts "" puts "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}' :(" 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..." 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." 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!" 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 resolveDependencies search origin, true build_package Dir.pwd rescue InstallError => e abort "#{@pkg.name} failed to build: #{e.to_s}" 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" if @pkg.is_fake? abort "It is not possible to build without source" 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" end puts "#{pkg_name} is built!" 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." 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!" end case @command when "help" if @pkgName help @pkgName else puts "Usage: crew help [command]" help nil end when "search" if @pkgName 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.2" puts "Usage: crew [command] [package]" help nil else puts "I have no idea how to do #{@command} :(" help nil end