require "uuid" require "uri" require "file_utils" require "spec" require "./context.cr" require "./package.cr" require "./instructions.cr" require "./sources.cr" # 🤔 class URI def basename File.basename path end end 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 end # FIXME: This could probably be shorter and less stupid with a macro or two. class Specs::StringContainer def as_s : String value end def as_a_or_s : Array(String) # FIXME: We should probably be splitting the string around comas. [value] end def as_s_or_ls : String value end end class Specs::LongStringContainer def as_s : String raise "short string expected, got multiline text section" end def as_a_or_s : Array(String) raise "list or string expected, got multiline text section" end def as_s_or_ls : String value end end class Specs::ArrayContainer def as_s : String raise "string expected, got list" end def as_a_or_s : Array(String) value end def as_s_or_ls : String raise "string or multiline text section expected, got list" end end class Specs::SectionContainer def as_s : String raise "string expected, got section" end def as_a_or_s : Array(String) raise "list or string expected, got section" end def as_s_or_ls : String raise "string or multiline text section expected, got section" 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 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 @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 instance end def initialize(@context, specs : Specs) name : String? = nil version : String? = nil 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 "configurue" @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 "dependencies" value.as_a_or_s.each do |atom| @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 puts "WARNING: misformed option: #{option}" next end name, value = match @options[name] = value end end end raise "`name` was not provided" unless name raise "`version` was not provided" unless version @name = name @version = version end def self.new(context, specs : Specs) instance = Recipe.allocate.tap &.initialize(context, specs) instance.packages << Package.new instance instance end def working_directory "#{@context.working_directory}/#{@working_uuid}" end def building_directory "#{working_directory}/build" end def fake_root_directory @packages[0].fake_root_directory end def dirname @dirname || "#{name}-#{version}" end def download : Bool sources .compact_map do |url| unless File.exists? url.basename @context.run @context.sources_directory, "wget", [ url.to_s, "-O", url.basename ] end end .map(&.success?) .reduce(true) { |a, b| a && b } end def extract : Bool Dir.mkdir_p building_directory sources .map do |url| basename = url.basename @context.run building_directory, "bsdtar", [ "xvf", @context.sources_directory + "/" + url.basename ] end .map(&.success?) .reduce true { |a, b| a && b } 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 : Bool success = true Dir.mkdir_p fake_root_directory ENV["PKG"] = fake_root_directory # Safety precautions. old_dir = Dir.current instructions.to_a.each do |instruction| if instruction.run(@context, self).failed? break BuildStatus::Failed end end Dir.cd old_dir ENV["PKG"] = nil do_splits success 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] pp! file files_to_split << file if file_patterns.any? &.match file 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}" Dir.mkdir_p File.dirname destination FileUtils.mv( "#{fake_root_directory}#{file}", "#{package.fake_root_directory}#{file}" ) puts file @context.run "find", [ "#{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 : Bool # This tries to build them all and stops at the first failure # (failures are currently reported by Context#package) (@packages + auto_splits) .find do |package| if package.automatic && ! File.exists? package.fake_root_directory next end ! @context.package package end .== nil end def clean FileUtils.rm_rf building_directory FileUtils.rm_rf fake_root_directory end def to_s "#{name}-#{version}" end end