require "uuid" require "uri" require "file_utils" require "specfileparser" require "./context.cr" require "./package.cr" require "./instructions.cr" require "./sources.cr" require "./exception.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 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 @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 = SpecFileParser.parse filename, { "pkg" => fake_root_directory } name : String? = nil version : String? = nil raise "Could not parse `#{filename}`" if specs.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 "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 "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 puts "WARNING: misformed option: #{option}" next end key, value = match @options[key] = value end 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| if section.name == "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 download sources.each do |url| unless File.exists? url.basename status = @context.run @context.sources_directory, "wget", [ url.to_s, "-O", url.basename ] raise DownloadError.new self, url unless status.success? end end end def extract Dir.mkdir_p building_directory sources.each do |url| basename = url.basename status = @context.run( building_directory, "bsdtar", [ "xvf", @context.sources_directory + "/" + url.basename ] ) raise ExtractionError.new self, url unless status.success? 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| 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_splits 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 # 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 # 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 unless @context.package package raise PackagingError.new self, package end end end def clean FileUtils.rm_rf building_directory FileUtils.rm_rf fake_root_directory 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