345 lines
8.5 KiB
Crystal
345 lines
8.5 KiB
Crystal
require "colorize"
|
||
|
||
require "specparser"
|
||
|
||
require "./exception.cr"
|
||
|
||
require "./config.cr"
|
||
|
||
require "./backends/baguette.cr"
|
||
require "./backends/apk.cr"
|
||
require "./backends/pkgutils.cr"
|
||
require "./backends.cr"
|
||
|
||
# FIXME: Where should this go? We can’t just leave it here. :(
|
||
def pkginfo(package)
|
||
du = `du -sk #{package.fake_root_directory}`
|
||
size = du.sub(/[ \t].*/, "").to_i * 1024
|
||
|
||
lines = [] of String
|
||
|
||
lines << "# Generated by `packaging`."
|
||
lines << "pkgname = #{package.name}"
|
||
lines << "pkgver = #{package.version}-r#{package.release}"
|
||
lines << "url = #{package.url || ""} "
|
||
lines << "size = #{size}"
|
||
lines << "origin = #{package.recipe.name}"
|
||
lines << "buildtype = host" # This’ll need to be imported from Context.
|
||
lines << "builddate = #{Time.utc.to_unix}"
|
||
|
||
package.dependencies.each do |atom|
|
||
lines << "depend = #{atom.to_s}"
|
||
end
|
||
|
||
package.provides.each do |atom|
|
||
lines << "provides = #{atom.to_s}"
|
||
end
|
||
|
||
package.conflicts.each do |atom|
|
||
lines << "conflicts = #{atom.to_s}"
|
||
end
|
||
|
||
lines.join("\n") + "\n"
|
||
end
|
||
|
||
class Package::Context
|
||
property working_directory = "/tmp/packaging"
|
||
property sources_directory = Dir.current
|
||
property packages_directory = Dir.current
|
||
|
||
getter packaging_backends = [] of Backend::Packaging
|
||
getter building_backends = [] of Backend::Building
|
||
getter splitter_backends = [] of Backend::Splitter
|
||
|
||
# Directories where ports can be found.
|
||
getter repositories = [] of String
|
||
|
||
# Well, this will need configuration, auto-detection and conversion,
|
||
# but it’ll be kind of enough for now.
|
||
property architecture = "x86_64"
|
||
|
||
# prefixes for `packaging` running environment and child processes
|
||
# = where to search for binaries and libraries for the build
|
||
property prefixes = ["/usr", "/", "/usr/baguette"]
|
||
|
||
# By default, building a package only uses one core
|
||
property build_cores = 1
|
||
|
||
# list of environment variables we want to have when building
|
||
property environment = {} of String => String
|
||
|
||
property verbosity = 0
|
||
|
||
property recipe : Recipe? = nil
|
||
|
||
|
||
def initialize
|
||
@packaging_backends << ApkBackend.new
|
||
@packaging_backends << BaguetteBackend.new
|
||
@packaging_backends << PkgutilsBackend.new
|
||
|
||
@selected_packaging_backend = @packaging_backends[0]
|
||
|
||
@building_backends << Backend::Building.new "configure", "autotools" do |context, recipe|
|
||
next BuildStatus::Pass unless Dir.exists? recipe.dirname
|
||
|
||
Dir.cd recipe.dirname
|
||
|
||
unless File.exists? "configure"
|
||
next BuildStatus::Pass
|
||
end
|
||
|
||
child = context.sh "./configure --prefix=#{recipe.prefix} #{recipe.options["configure"]? || ""}"
|
||
|
||
if child.exit_status == 0
|
||
BuildStatus::Success
|
||
else
|
||
BuildStatus::Failed
|
||
end
|
||
end
|
||
|
||
@building_backends << Backend::Building.new "configure", "cmake" do |context, recipe|
|
||
next BuildStatus::Pass unless Dir.exists? recipe.dirname
|
||
|
||
Dir.cd recipe.dirname
|
||
|
||
next BuildStatus::Pass unless File.exists? "CMakeLists.txt"
|
||
|
||
options = [
|
||
"-DCMAKE_INSTALL_PREFIX='#{recipe.prefix}'",
|
||
"-DCMAKE_BUILD_TYPE=Release #{recipe.options["cmake"]}",
|
||
"-- -j#{context.build_cores}"
|
||
]
|
||
|
||
child = context.sh "cmake . #{options.join " "}"
|
||
if child.exit_status == 0
|
||
BuildStatus::Success
|
||
else
|
||
BuildStatus::Failed
|
||
end
|
||
end
|
||
|
||
@building_backends << Backend::Building.new "build", "make" do |context, recipe|
|
||
next BuildStatus::Pass unless Dir.exists? recipe.dirname
|
||
Dir.cd recipe.dirname
|
||
|
||
unless File.exists? "Makefile"
|
||
next BuildStatus::Pass
|
||
end
|
||
|
||
child = context.sh "make -j#{context.build_cores} #{recipe.options["make"]? || ""}"
|
||
|
||
if child.exit_status == 0
|
||
BuildStatus::Success
|
||
else
|
||
BuildStatus::Failed
|
||
end
|
||
end
|
||
|
||
@building_backends << Backend::Building.new "install", "make" do |context, recipe|
|
||
next BuildStatus::Pass unless Dir.exists? recipe.dirname
|
||
Dir.cd recipe.dirname
|
||
|
||
unless File.exists? "Makefile"
|
||
next BuildStatus::Pass
|
||
end
|
||
|
||
child = context.sh "make install 'DESTDIR=#{recipe.fake_root_directory}' #{recipe.options["make install"]? || ""}"
|
||
|
||
if child.exit_status == 0
|
||
BuildStatus::Success
|
||
else
|
||
BuildStatus::Failed
|
||
end
|
||
end
|
||
|
||
# Splitters = creating new packages:
|
||
# keep prefixes
|
||
# split names (man, src, ...)
|
||
# split files
|
||
|
||
@splitter_backends << Backend::Splitter.new do |recipe|
|
||
Package.new(recipe, true).tap do |split|
|
||
prefixes = (@prefixes + [recipe.prefix]).uniq
|
||
|
||
split.name = "#{recipe.name}-man"
|
||
split.files = prefixes.map do |prefix|
|
||
"#{prefix}/share/man"
|
||
end
|
||
end
|
||
end
|
||
|
||
@splitter_backends << Backend::Splitter.new do |recipe|
|
||
Package.new(recipe, true).tap do |split|
|
||
prefixes = (@prefixes + [recipe.prefix]).uniq
|
||
|
||
split.name = "#{recipe.name}-dev"
|
||
split.files = prefixes.map do |prefix|
|
||
[
|
||
"#{prefix}/include",
|
||
"#{prefix}/lib/pkgconfig"
|
||
]
|
||
end.flatten
|
||
split.file_patterns = prefixes.map do |prefix|
|
||
Regex.new("^" + prefix + ".*\\.a$")
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
def packaging_backend=(name : String)
|
||
@selected_packaging_backend = @packaging_backends.find(&.name.==(name)).not_nil!
|
||
end
|
||
|
||
def packaging_backend=(backend : Backend::Packaging)
|
||
@selected_packaging_backend = backend
|
||
end
|
||
|
||
def run(chdir, command, args)
|
||
output = Process::Redirect::Inherit
|
||
|
||
if @verbosity < -1
|
||
output = Process::Redirect::Close
|
||
else
|
||
# log sub-commands outputs
|
||
logfile_path = "#{@working_directory}/#{@recipe.not_nil!.working_uuid}.log"
|
||
output = File.open logfile_path, "a"
|
||
STDOUT.puts "logging command #{command} #{args}"
|
||
STDOUT.puts "in " + logfile_path.colorize(:blue).mode(:bright).to_s
|
||
|
||
output.puts ""
|
||
output.puts ""
|
||
output.puts "logging command $ #{command}"
|
||
output.puts " parameters $ #{args}"
|
||
end
|
||
|
||
c = Process.run command, args, chdir: chdir, output: output, error: output, env: @environment
|
||
|
||
case output
|
||
when File
|
||
output.close
|
||
end
|
||
|
||
c
|
||
end
|
||
|
||
def run(command, args)
|
||
run nil, command, args
|
||
end
|
||
|
||
def run(command)
|
||
run nil, command, nil
|
||
end
|
||
|
||
def sh(command)
|
||
run nil, "sh", ["-x", "-e", "-c", command]
|
||
end
|
||
|
||
def captured_sh(command)
|
||
output = IO::Memory.new
|
||
child = Process.run "sh", ["-x", "-e", "-c", command], output: output
|
||
|
||
{child, output}
|
||
end
|
||
|
||
# Output functions.
|
||
def title(s)
|
||
return if @verbosity < -3
|
||
puts ">> ".colorize(:green).mode(:bright).to_s +
|
||
s.colorize(:white).mode(:bright).to_s
|
||
STDOUT.flush
|
||
end
|
||
def info(s)
|
||
return if @verbosity < -2
|
||
puts ":: ".colorize(:green).to_s + s.colorize(:white).to_s
|
||
STDOUT.flush
|
||
end
|
||
def detail(s)
|
||
return if @verbosity < -1
|
||
puts ("+ " + s).colorize(:cyan)
|
||
STDOUT.flush
|
||
end
|
||
|
||
def package(package : Package) : Bool
|
||
@selected_packaging_backend.package self, package
|
||
end
|
||
|
||
def read_recipe(filename : String)
|
||
@recipe = Recipe.new self, filename
|
||
@recipe.not_nil!
|
||
end
|
||
|
||
def find_recipe(name : String) : Recipe?
|
||
recipe_file_name = ""
|
||
|
||
repo = @repositories.find do |repo|
|
||
repo_dir_name = "#{repo}/#{name}"
|
||
recipe_file_name = "#{repo_dir_name}/recipe.spec"
|
||
|
||
if Dir.exists?(repo_dir_name) && File.exists?(recipe_file_name)
|
||
next true
|
||
end
|
||
end
|
||
|
||
read_recipe recipe_file_name if repo
|
||
end
|
||
|
||
def read_configuration(filename : String)
|
||
specs = SpecParser.parse(File.read(filename))
|
||
|
||
specs.assignments.each do |key, value|
|
||
case key
|
||
when "packages-directory"
|
||
@packages_directory = value.as_s
|
||
when "sources-directory"
|
||
@sources_directory = value.as_s
|
||
when "working-directory"
|
||
@working_directory = value.as_s
|
||
when "prefixes"
|
||
# Prefixes during the build process.
|
||
@prefixes = value.as_a_or_s
|
||
when "build-cores"
|
||
# NB of cores used to compile applications.
|
||
@build_cores = value.as_s.to_i
|
||
|
||
when "environment"
|
||
# Environment variables during the build process.
|
||
value.as_a_or_s.each do |entry|
|
||
match = entry.split(':').map(
|
||
&.gsub(/^[ \t]*/, "").gsub(/[ \t]*$/, ""))
|
||
|
||
if match.size != 2
|
||
STDERR.puts "WARNING: misformed environment definition: #{entry}"
|
||
next
|
||
end
|
||
|
||
key, value = match
|
||
@environment[key] = value
|
||
if @verbosity > 0
|
||
STDOUT.puts "environment: #{key} => #{value}"
|
||
end
|
||
end
|
||
|
||
when "package-manager"
|
||
# Targeted package manager (default: package, for BaguetteOS).
|
||
begin
|
||
self.packaging_backend = value.as_s
|
||
rescue e
|
||
STDERR.puts "Error during selecting packaging backend: #{e}"
|
||
STDERR.puts "#{value.as_s} seems not to be a valid backend"
|
||
STDERR.puts "valid backends:"
|
||
@packaging_backends.each do |backend|
|
||
STDERR.puts "- #{backend.name}"
|
||
end
|
||
exit 1
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
def install(packages : Array(String))
|
||
@selected_packaging_backend.install packages
|
||
end
|
||
end
|
||
|