This repository has been archived on 2022-01-17. You can view files and clone it, but cannot push or open issues/pull-requests.
packaging/src/recipe.cr

491 lines
11 KiB
Crystal

require "uuid"
require "uri"
require "file_utils"
require "specparser"
require "./context.cr"
require "./package.cr"
require "./instructions.cr"
require "./sources.cr"
require "./exception.cr"
module FileUtils
def self.find(directory, &block : Proc(String, Nil))
Dir.each_child directory do |child|
child_path = directory + "/" + child
yield child_path
if File.directory? child_path
self.find child_path, &block
end
end
end
def self.find_files(directory, &block : Proc(String, Nil))
Dir.each_child directory do |child|
child_path = directory + "/" + child
if File.file? child_path
yield child_path
end
if File.directory? child_path
self.find child_path, &block
end
end
end
end
class Package::Recipe
@context : Context
# Core recipe informations.
getter name : String
getter version : String
getter release = 1
property url : String?
property description : String?
# Informations not specific to individual packages.
getter sources : Sources
getter packages : Array(Package)
property packager : String?
property maintainer : String?
getter contributors = Array(String).new
# Relations to other packages.
# FIXME: `dependencies` needs splitting between run-time and build-time.
getter run_dependencies = Array(String).new
getter build_dependencies = Array(String).new
getter provides = Array(String).new
getter conflicts = Array(String).new
# Build instructions, helpers and other build-only data.
setter dirname : String? # matching getter defined manually later.
getter instructions = Instructions.new
getter options = Hash(String, String).new
getter sources = Sources.new
getter packages = Array(Package).new
getter watch_script : String?
@prefix : String?
property recipe_directory = "."
@working_uuid : UUID = UUID.random
def initialize(@context, @name, @version)
end
def self.new(context, name, version)
instance = Recipe.allocate.tap &.initialize(context, name, version)
instance.packages << Package.new instance, false, instance.fake_root_directory
instance
end
def initialize(@context, filename : String)
specs = SpecParser.parse filename, {
"pkg" => fake_root_directory,
"prefix" => prefix
}
name : String? = nil
version : String? = nil
@recipe_directory = File.dirname filename
specs.assignments.each do |key, value|
case key
when "name"
name = value.as_s
when "version"
version = value.as_s
when "release"
@release = value.as_s.to_i
when "url"
@url = value.as_s
when "description"
@description = value.as_s_or_ls
when "packager"
@packager = value.as_s
when "maintainer"
@maintainer = value.as_s
when "sources"
value.as_a_or_s.each do |source|
@sources << source
end
when "configure"
@instructions.configure << value.as_s_or_ls
when "build"
@instructions.build << value.as_s_or_ls
when "install"
@instructions.install << value.as_s_or_ls
when "dirname"
@dirname = value.as_s
when "prefix"
@prefix = value.as_s
when "dependencies"
value.as_a_or_s.each do |atom|
@run_dependencies << atom
@build_dependencies << atom
end
when "build-dependencies"
value.as_a_or_s.each do |atom|
@build_dependencies << atom
end
when "run-dependencies"
value.as_a_or_s.each do |atom|
@run_dependencies << atom
end
when "conflicts"
value.as_a_or_s.each do |atom|
@conflicts << atom
end
when "provides"
value.as_a_or_s.each do |atom|
@provides << atom
end
when "options"
value.as_a_or_s.each do |option|
match = option.split(':').map(
&.gsub(/^[ \t]*/, "").gsub(/[ \t]*$/, ""))
if match.size != 2
STDERR.puts "WARNING: misformed option: #{option}"
next
end
key, value = match
@options[key] = value
end
when "watch"
@watch_script = value.as_s_or_ls
end
end
raise "`name` was not provided" unless name
raise "`version` was not provided" unless version
@name = name
@version = version
# Packages can only be created once @name and @version exist!
@packages << Package.new self, false, fake_root_directory
specs.sections.each do |section|
case section.name
when "split"
@packages << Package.new self, section
end
end
end
def working_directory
"#{@context.working_directory}/#{@working_uuid}"
end
def building_directory
"#{working_directory}/build"
end
def fake_root_directory
"#{working_directory}/root"
end
def dirname
@dirname || "#{name}-#{version}"
end
def prefix : String
@prefix || @context.prefixes[0]? || "/usr"
end
def download
sources.each do |url|
next if url.scheme == "file"
filename = @context.sources_directory + "/" + url.filename
unless File.exists? filename
@context.info "Downloading '#{url.filename}'"
status = @context.run @context.sources_directory, "wget", [ url.to_s, "-O", filename ]
raise DownloadError.new self, url unless status.success?
end
end
end
def extract
Dir.mkdir_p building_directory
sources.each do |url|
basename = url.filename
if basename.match /\.(tar\.(gz|xz|bz2|lzma)|tgz)$/
@context.info "Extracting '#{url.filename}'"
status = @context.run(
building_directory,
"bsdtar", [
"xf",
@context.sources_directory + "/" + url.filename
]
)
raise ExtractionError.new self, url unless status.success?
elsif basename.match /\.patch$/
@context.info "Applying '#{url.filename}'"
status = @context.run(
"#{building_directory}/#{dirname}",
"patch", [
"-i", "#{@context.sources_directory}/#{url.filename}"
]
)
raise ExtractionError.new self, url unless status.success?
else
@context.info "Copying '#{url.filename}'"
directory = if url.scheme == "file"
@recipe_directory
else
@context.sources_directory
end
FileUtils.cp "#{directory}/#{url.filename}",
"#{building_directory}/#{url.filename}"
end
end
end
# TODO:
# - Export packaging directory in $PKG.
# - Add (pre|post)-(configure|build|install) instructions.
# - Have some instructions be non-critical, like the (pre|post] ones.
# - Be careful about return values, flee from everything if something
# goes somehow wrong.
# - Make things thread-safe. (those ENV[]= calls are definitely not)
def build
Dir.mkdir_p fake_root_directory
ENV["PKG"] = fake_root_directory
# Safety precautions.
old_dir = Dir.current
instructions.to_a.each do |instruction|
@context.info "Building ('#{instruction.phase}' phase)"
if instruction.run(@context, self).failed?
raise BuildError.new self, "Building (#{instruction.phase} phase) failed."
break
end
end
Dir.cd old_dir
ENV["PKG"] = nil
if Dir.children(fake_root_directory).size == 0
raise BuildError.new self, "No file was installed in the fake root."
end
do_strip
do_splits
end
private def do_strip
@context.info "Stripping binaries"
FileUtils.find_files(fake_root_directory) do |path|
file_output = `file #{path}`
strip_opt = nil : String?
if file_output.match /ELF.*executable.*not stripped/
strip_opt = "--strip-all"
elsif file_output.match /ELF.*shared object.*not stripped/
strip_opt = "--strip-unneeded"
elsif file_output.match /current ar archive/
strip_opt = "--strip-debug"
end
if strip_opt
@context.detail "stripping #{path}"
Process.run "strip", [strip_opt, path]
end
end
end
private def do_splits
(@packages + auto_splits).each do |package|
next if package == @packages[0]
files = package.files || [] of String
file_patterns = package.file_patterns || [] of Regex
files_to_split = [] of String
files.each do |file|
origin = "#{fake_root_directory}#{file}"
if File.exists? origin
files_to_split << file
end
end
FileUtils.find fake_root_directory do |file|
file = file[fake_root_directory.size..file.size]
files_to_split << file if file_patterns.any? &.match file
end
if files_to_split.size > 0
@context.info "Splitting '#{package.name}'"
end
# FIXME: What do we do if those are not on the filesystem?
files_to_split.each do |file|
origin = "#{fake_root_directory}#{file}"
destination ="#{package.fake_root_directory}#{file}"
@context.detail "Moving '#{file}' to split"
Dir.mkdir_p File.dirname destination
FileUtils.mv(
"#{fake_root_directory}#{file}",
"#{package.fake_root_directory}#{file}"
)
end
end
end
def auto_splits : Array(Package)
@context.splitter_backends.compact_map do |backend|
backend.create_split self
end
end
# TODO:
# - Errors management. Stop at first failure?
# - Splits. This should be done between #build and #package.
def package
# This tries to build them all and stops at the first failure
# (failures are currently reported by Context#package)
(@packages + auto_splits).each do |package|
if package.automatic && ! File.exists? package.fake_root_directory
next
end
@context.info "Assempling '#{package.name}'"
unless @context.package package
raise PackagingError.new self, package
end
end
end
def clean
FileUtils.rm_rf working_directory
end
def watch
if script = @watch_script
status, output = @context.captured_sh script
output = output.to_s
.gsub(/^[ \t\n]*/, "").gsub(/[ \t\n]*$/, "")
if output != @version
STDERR.puts "WARNING: '#{@name}' is old! (#{output} / #{@version})"
else
puts "'#{@name}' is up to date"
end
return
end
STDERR.puts "WARNING: '#{@name}' has no watch."
end
def to_s
"#{name}-#{version}"
end
def to_spec_s
spec = [
"name: #{name}",
"version: #{version}",
"release: #{release}"
].join("\n") + "\n"
if @sources.size == 1
spec += "sources: #{sources[0].to_s.gsub @version, "%{version}"}\n"
end
if maintainer
spec += "maintainer: #{maintainer}\n"
end
if packager
spec += "packager: #{packager}\n"
end
if @options.size > 0
spec += "options:\n"
@options.each do |key, value|
value = value.gsub fake_root_directory, "%{pkg}"
spec += "\t- #{key}: #{value}\n"
end
spec += "\n" # Limitation of the current implementation.
end
if @dependencies.size > 0
spec += "dependencies:\n"
@dependencies.each do |dep|
spec += "\t- #{dep}\n"
end
spec += "\n"
end
if @dirname
spec += "dirname: #{@dirname}\n"
end
[
{"configure", @instructions.configure},
{"build", @instructions.build},
{"install", @instructions.install}
].each do |name, instructions|
if instructions.size > 0
spec += "@#{name}\n"
instructions.each do |line|
line = line.gsub /^\t* *\n/, ""
line = line.gsub /\n *\t*\n/, "\n"
line = line.gsub /\n *\t*$/, ""
line = line.gsub fake_root_directory, "%{pkg}"
line = line.gsub name, "%{name}"
line = line.gsub version, "%{version}"
line = line.gsub dirname, "%{dirname}"
line = line.gsub(/^[\t ]*/, "").gsub(/\n\t*/, "\n\t")
spec += "\t#{line}\n"
end
spec += "\n" # Limitation of the current implementation.
end
end
spec
end
end