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" 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 # Package::Backend::Splitter = create new package # takes (then stores) the given block # this block takes a recipe as a parameter and create a new package # the new package: # keep prefixes # new name = split name (-man, -src, ...) # split files # Man-pages and documentation @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 split.recipe.require_stripping = false end end # Developer's files: headers, pkgconfig files, *.a. @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 # Any prefix containing "/include/" split.file_patterns.not_nil! << Regex.new(".*/include/.*/") end end # Source files: prefix containing "src". @splitter_backends << Backend::Splitter.new do |recipe| Package.new(recipe, true).tap do |split| prefixes = (@prefixes + [recipe.prefix]).uniq split.name = "#{recipe.name}-src" split.files = prefixes.map do |prefix| [ "#{prefix}/lib/share/src", "#{prefix}/src", "#{prefix}/usr/src", "#{prefix}/usr/local/src" ] end.flatten split.recipe.require_stripping = false 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}".colorize(:light_magenta).to_s 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 # Log file moves during splits. def mv(f1 : String, f2 : String) run "mv", [ f1, f2 ] end # Log directory creations during splits. def mkdir_p(dir : String) run "mkdir", [ "-p", dir ] 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