Files
chromebrew/crew

779 lines
21 KiB
Ruby
Executable File

#!/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 <packageName> 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 <packageName>' 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