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 File.read(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