Massive refactoring. See more in git log.
This refactoring includes: - the use of baguette-crystal-base (Baguette::Log, Baguette::Context for verbosity and logfile) - moving configure, build, install and splits backends into dedicated files - removing a lot of code from Package::Context (it was a mess, still kinda is) - fix 32-bit integer bug from APK backend - some API changes in packaging backends to remove the context instance it was replaced by two parameters: package directory and architecture - Baguette, APK and PkgUtils now share (kinda) the same coding style
This commit is contained in:
parent
e0d3c6bf7e
commit
8392363d49
@ -5,7 +5,7 @@ abstract class Package::Backend::Packaging
|
|||||||
def initialize(@name)
|
def initialize(@name)
|
||||||
end
|
end
|
||||||
|
|
||||||
abstract def package(context : Context, package : Package) : Bool
|
abstract def package(pkgdir : String, architecture : String, package : Package) : Bool
|
||||||
def self.install(packages : Array(String))
|
def self.install(packages : Array(String))
|
||||||
raise "'install' unimplemented for this backend, yet"
|
raise "'install' unimplemented for this backend, yet"
|
||||||
end
|
end
|
||||||
@ -14,13 +14,4 @@ abstract class Package::Backend::Packaging
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Package::Backend::Splitter
|
require "./backends/*"
|
||||||
def initialize(&block : Proc(Recipe, Package))
|
|
||||||
@callback = block
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_split(recipe : Recipe) : Package
|
|
||||||
@callback.call recipe
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,23 +1,25 @@
|
|||||||
require "../backends.cr"
|
require "../backends.cr"
|
||||||
|
|
||||||
class ApkBackend < Package::Backend::Packaging
|
class Package::Backend::Packaging::APK < Package::Backend::Packaging
|
||||||
def initialize
|
def initialize
|
||||||
@name = "apk"
|
@name = "apk"
|
||||||
end
|
end
|
||||||
|
|
||||||
def package(context : Package::Context, package : Package::Package) : Bool
|
def package(pkgdir : String, architecture : String, package : ::Package::Package) : Bool
|
||||||
# FIXME: This needs to have access to architecture (from Context?)
|
# FIXME: This needs to have access to architecture (from Context?)
|
||||||
# to work properly.
|
# to work properly.
|
||||||
old_cwd = Dir.current
|
old_cwd = Dir.current
|
||||||
|
|
||||||
File.write "#{package.fake_root_directory}/.PKGINFO", ApkBackend.pkginfo package
|
APK.pkginfo package, "#{package.fake_root_directory}/.PKGINFO"
|
||||||
|
|
||||||
# Create data.tar.gz here.
|
# Create data.tar.gz here.
|
||||||
package_target = "#{context.packages_directory}/#{context.architecture}/#{package.name}-#{package.version}-r#{package.release}.apk"
|
package_target = "#{pkgdir}/"
|
||||||
|
package_target += "#{architecture}/"
|
||||||
|
package_target += "#{package.name}-#{package.version}-r#{package.release}.apk"
|
||||||
Dir.mkdir_p File.dirname package_target
|
Dir.mkdir_p File.dirname package_target
|
||||||
|
|
||||||
# FIXME: This shouldn’t have to be in users’ PATH. libexec?
|
# FIXME: This shouldn’t have to be in users’ PATH. libexec?
|
||||||
r = context.run package.fake_root_directory, "#{OWN_LIBEXEC_DIR}/assemble-apk.sh", [
|
r = Do.run package.fake_root_directory, "#{OWN_LIBEXEC_DIR}/assemble-apk.sh", [
|
||||||
package_target
|
package_target
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -25,34 +27,32 @@ class ApkBackend < Package::Backend::Packaging
|
|||||||
end
|
end
|
||||||
|
|
||||||
# This generated file content is specific to the Apk package manager.
|
# This generated file content is specific to the Apk package manager.
|
||||||
def self.pkginfo(package)
|
def self.pkginfo(package : ::Package::Package, file_name : String)
|
||||||
du = `du -sk #{package.fake_root_directory}`
|
du = `du -sk #{package.fake_root_directory}`
|
||||||
size = du.sub(/[ \t].*/, "").to_i * 1024
|
size = du.sub(/[ \t].*/, "").to_u64 * 1024_u64
|
||||||
|
|
||||||
lines = [] of String
|
File.open file_name, "w" do |file|
|
||||||
|
file.puts "# Generated by `packaging`."
|
||||||
lines << "# Generated by `packaging`."
|
file.puts "pkgname = #{package.name}"
|
||||||
lines << "pkgname = #{package.name}"
|
file.puts "pkgver = #{package.version}-r#{package.release}"
|
||||||
lines << "pkgver = #{package.version}-r#{package.release}"
|
file.puts "url = #{package.url || ""} "
|
||||||
lines << "url = #{package.url || ""} "
|
file.puts "size = #{size}"
|
||||||
lines << "size = #{size}"
|
file.puts "origin = #{package.recipe.name}"
|
||||||
lines << "origin = #{package.recipe.name}"
|
file.puts "buildtype = host" # This’ll need to be imported from Context.
|
||||||
lines << "buildtype = host" # This’ll need to be imported from Context.
|
file.puts "builddate = #{Time.utc.to_unix}"
|
||||||
lines << "builddate = #{Time.utc.to_unix}"
|
|
||||||
|
|
||||||
package.dependencies.each do |atom|
|
package.dependencies.each do |atom|
|
||||||
lines << "depend = #{atom.to_s}"
|
file.puts "depend = #{atom.to_s}"
|
||||||
end
|
end
|
||||||
|
|
||||||
package.provides.each do |atom|
|
package.provides.each do |atom|
|
||||||
lines << "provides = #{atom.to_s}"
|
file.puts "provides = #{atom.to_s}"
|
||||||
end
|
end
|
||||||
|
|
||||||
package.conflicts.each do |atom|
|
package.conflicts.each do |atom|
|
||||||
lines << "conflicts = #{atom.to_s}"
|
file.puts "conflicts = #{atom.to_s}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
lines.join("\n") + "\n"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Install programs.
|
# Install programs.
|
||||||
|
@ -3,38 +3,43 @@ require "openssl"
|
|||||||
require "../backends.cr"
|
require "../backends.cr"
|
||||||
|
|
||||||
|
|
||||||
class BaguetteBackend < Package::Backend::Packaging
|
class Package::Backend::Packaging::Baguette < Package::Backend::Packaging
|
||||||
def initialize
|
def initialize
|
||||||
@name = "package"
|
@name = "package"
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: checksums + security
|
# TODO: checksums + security
|
||||||
def package(context : Package::Context, package : Package::Package) : Bool
|
def package(pkgdir : String, architecture : String, package : ::Package::Package) : Bool
|
||||||
# Fake root example: /tmp/packages/<uuid>/root-xz-dev
|
# Fake root example: /tmp/packages/<uuid>/root-xz-dev
|
||||||
fake_root = package.fake_root_directory
|
fake_root = package.fake_root_directory
|
||||||
|
|
||||||
data_archive_path = "#{fake_root}/data.tar"
|
# Temporary archive, before compression.
|
||||||
compressed_data_archive_path = "#{fake_root}/data.tar.zst"
|
data_archive_path = "#{fake_root}/data.tar" # All archive data.
|
||||||
control_spec_file_path = "#{fake_root}/control.spec"
|
# Content of the final tarball.
|
||||||
manifest_file_path = "#{fake_root}/manifest"
|
compressed_data_archive_path = "#{fake_root}/data.tar.zst" # Compressed version (in final tarball).
|
||||||
|
control_spec_file_path = "#{fake_root}/control.spec" # Spec file, with package informations.
|
||||||
|
manifest_file_path = "#{fake_root}/manifest" # List of all the included files.
|
||||||
|
|
||||||
package_target = "#{context.packages_directory}/#{package.name}-#{package.version}-#{package.release}.baguette"
|
package_target = "#{pkgdir}/"
|
||||||
|
package_target += "#{architecture}/"
|
||||||
|
package_target += "#{package.name}-#{package.version}-#{package.release}.baguette"
|
||||||
|
Dir.mkdir_p File.dirname package_target
|
||||||
|
|
||||||
context.detail "Archiving package content"
|
::Baguette::Log.detail "Archiving package content"
|
||||||
context.run fake_root, "tar", ["cvf", data_archive_path, "."]
|
Do.run fake_root, "tar", ["cvf", data_archive_path, "."]
|
||||||
|
|
||||||
context.detail "Compressing the archive"
|
::Baguette::Log.detail "Compressing the archive"
|
||||||
# produces data.tar.zst
|
# produces data.tar.zst
|
||||||
context.run fake_root, "zstd", ["--ultra", data_archive_path]
|
Do.run fake_root, "zstd", ["--ultra", data_archive_path]
|
||||||
|
|
||||||
context.detail "Generating control.spec"
|
::Baguette::Log.detail "Generating control.spec"
|
||||||
generate_spec package, control_spec_file_path
|
generate_spec package, control_spec_file_path
|
||||||
|
|
||||||
context.detail "Generating manifest"
|
::Baguette::Log.detail "Generating manifest"
|
||||||
generate_manifest context, package, manifest_file_path
|
generate_manifest package, manifest_file_path
|
||||||
|
|
||||||
context.detail "Assembling '#{package_target}'"
|
::Baguette::Log.detail "Assembling '#{package_target}'"
|
||||||
r = context.run fake_root, "tar", [
|
r = Do.run fake_root, "tar", [
|
||||||
"cf", package_target,
|
"cf", package_target,
|
||||||
# WARNING: relative paths are necessary.
|
# WARNING: relative paths are necessary.
|
||||||
"control.spec", "manifest", "data.tar.zst"
|
"control.spec", "manifest", "data.tar.zst"
|
||||||
@ -43,12 +48,11 @@ class BaguetteBackend < Package::Backend::Packaging
|
|||||||
r.exit_status == 0
|
r.exit_status == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_spec(package : Package::Package, file_name : String)
|
def generate_spec(package : ::Package::Package, file_name : String)
|
||||||
du = `du -sk #{package.fake_root_directory}`
|
du = `du -sk #{package.fake_root_directory}`
|
||||||
size = du.sub(/[ \t].*/, "").to_u64 * 1024_u64
|
size = du.sub(/[ \t].*/, "").to_u64 * 1024_u64
|
||||||
|
|
||||||
file = File.open file_name, "w"
|
File.open file_name, "w" do |file|
|
||||||
|
|
||||||
file.puts "name: #{package.name}"
|
file.puts "name: #{package.name}"
|
||||||
file.puts "version: #{package.version}"
|
file.puts "version: #{package.version}"
|
||||||
file.puts "release: #{package.release}"
|
file.puts "release: #{package.release}"
|
||||||
@ -63,11 +67,10 @@ class BaguetteBackend < Package::Backend::Packaging
|
|||||||
file.puts "dependencies: #{package.dependencies.join ", "}"
|
file.puts "dependencies: #{package.dependencies.join ", "}"
|
||||||
file.puts "conflicts: #{package.conflicts.join ", "}"
|
file.puts "conflicts: #{package.conflicts.join ", "}"
|
||||||
file.puts "provides: #{package.provides.join ", "}"
|
file.puts "provides: #{package.provides.join ", "}"
|
||||||
|
end
|
||||||
file.close
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_manifest(context : Package::Context, package : Package::Package, file_name : String)
|
def generate_manifest(package : ::Package::Package, file_name : String)
|
||||||
old_pwd = Dir.current
|
old_pwd = Dir.current
|
||||||
|
|
||||||
manifest = File.open(file_name, "w").not_nil!
|
manifest = File.open(file_name, "w").not_nil!
|
||||||
|
23
src/backends/build.cr
Normal file
23
src/backends/build.cr
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
class Package::Backend::Build
|
||||||
|
|
||||||
|
def self.make : Backend::Building
|
||||||
|
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 = Do.sh "make -j#{context.build_cores} #{recipe.options["make"]? || ""}"
|
||||||
|
|
||||||
|
if child.exit_status == 0
|
||||||
|
BuildStatus::Success
|
||||||
|
else
|
||||||
|
BuildStatus::Failed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
47
src/backends/configure.cr
Normal file
47
src/backends/configure.cr
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
class Package::Backend::Configure
|
||||||
|
|
||||||
|
def self.autotools : Backend::Building
|
||||||
|
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 = Do.sh "./configure --prefix=#{recipe.prefix} #{recipe.options["configure"]? || ""}"
|
||||||
|
|
||||||
|
if child.exit_status == 0
|
||||||
|
BuildStatus::Success
|
||||||
|
else
|
||||||
|
BuildStatus::Failed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.cmake : Backend::Building
|
||||||
|
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 = Do.sh "cmake . #{options.join " "}"
|
||||||
|
if child.exit_status == 0
|
||||||
|
BuildStatus::Success
|
||||||
|
else
|
||||||
|
BuildStatus::Failed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
23
src/backends/install.cr
Normal file
23
src/backends/install.cr
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
class Package::Backend::Install
|
||||||
|
def self.make : Backend::Building
|
||||||
|
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 = Do.sh "make install 'DESTDIR=#{recipe.fake_root_directory}' #{recipe.options["make install"]? || ""}"
|
||||||
|
|
||||||
|
if child.exit_status == 0
|
||||||
|
BuildStatus::Success
|
||||||
|
else
|
||||||
|
BuildStatus::Failed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
@ -1,13 +1,21 @@
|
|||||||
require "../backends.cr"
|
require "../backends.cr"
|
||||||
|
require "baguette-crystal-base"
|
||||||
|
|
||||||
class PkgutilsBackend < Package::Backend::Packaging
|
# This packaging backend only helps to create a package containing the data.
|
||||||
|
# This serves as the simplest example of packaging system:
|
||||||
|
# no extra data nor structure in the final archive.
|
||||||
|
|
||||||
|
class Package::Backend::Packaging::Pkgutils < Package::Backend::Packaging
|
||||||
def initialize
|
def initialize
|
||||||
@name = "pkgutils"
|
@name = "pkgutils"
|
||||||
end
|
end
|
||||||
|
|
||||||
def package(context : Package::Context, package : Package::Package) : Bool
|
def package(pkgdir : String, architecture : String, package : ::Package::Package) : Bool
|
||||||
puts "#{package.fake_root_directory} -> #{context.packages_directory}/#{package.name}##{package.version}-#{package.release}.pkg.tar.xz"
|
package_file = pkgdir + "/" + architecture + "/"
|
||||||
pp! r = context.run package.fake_root_directory, "tar", ["cJf", "#{context.packages_directory}/#{package.name}##{package.version}.pkg.tar.xz", "."]
|
package_file += "#{package.name}-#{package.version}-#{package.release}.pkg.tar.xz"
|
||||||
|
|
||||||
|
::Baguette::Log.info "#{package.fake_root_directory} -> #{package_file}"
|
||||||
|
pp! r = Do.run package.fake_root_directory, "tar", ["cJf", package_file, "."]
|
||||||
|
|
||||||
r.exit_status == 0
|
r.exit_status == 0
|
||||||
end
|
end
|
||||||
|
75
src/backends/splitter.cr
Normal file
75
src/backends/splitter.cr
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
|
||||||
|
class Package::Backend::Splitter
|
||||||
|
def initialize(&block : Proc(Recipe, Package))
|
||||||
|
@callback = block
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_split(recipe : Recipe) : Package
|
||||||
|
@callback.call recipe
|
||||||
|
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
|
||||||
|
def self.man(prefixes : Array(String)) : Splitter
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
|
# Developer's files: headers, pkgconfig files, *.a.
|
||||||
|
def self.dev(prefixes : Array(String)) : Splitter
|
||||||
|
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/.*.h(pp)?")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Source files: prefix containing "src".
|
||||||
|
def self.src(prefixes : Array(String)) : Splitter
|
||||||
|
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
|
||||||
|
end
|
242
src/context.cr
242
src/context.cr
@ -1,14 +1,8 @@
|
|||||||
require "colorize"
|
require "colorize"
|
||||||
|
|
||||||
require "specparser"
|
require "specparser"
|
||||||
|
|
||||||
require "./exception.cr"
|
require "./exception.cr"
|
||||||
|
|
||||||
require "./config.cr"
|
require "./config.cr"
|
||||||
|
|
||||||
require "./backends/baguette.cr"
|
|
||||||
require "./backends/apk.cr"
|
|
||||||
require "./backends/pkgutils.cr"
|
|
||||||
require "./backends.cr"
|
require "./backends.cr"
|
||||||
|
|
||||||
class Package::Context
|
class Package::Context
|
||||||
@ -29,157 +23,37 @@ class Package::Context
|
|||||||
|
|
||||||
# prefixes for `packaging` running environment and child processes
|
# prefixes for `packaging` running environment and child processes
|
||||||
# = where to search for binaries and libraries for the build
|
# = where to search for binaries and libraries for the build
|
||||||
property prefixes = ["/usr", "/", "/usr/baguette"]
|
property prefixes = ["/usr/baguette", "/usr", "/"]
|
||||||
|
|
||||||
# By default, building a package only uses one core
|
# By default, building a package only uses one core
|
||||||
property build_cores = 1
|
property build_cores = 1
|
||||||
|
|
||||||
# list of environment variables we want to have when building
|
# list of environment variables we want to have when building
|
||||||
property environment = {} of String => String
|
#property environment = {} of String => String
|
||||||
|
|
||||||
property verbosity = 0
|
|
||||||
|
|
||||||
property recipe : Recipe? = nil
|
property recipe : Recipe? = nil
|
||||||
|
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@packaging_backends << ApkBackend.new
|
# Add package backends: baguette (package-tools) and apk.
|
||||||
@packaging_backends << BaguetteBackend.new
|
# They implement the Backend::Packaging abstract class:
|
||||||
@packaging_backends << PkgutilsBackend.new
|
# init (@name) + package (context, package) method
|
||||||
|
@packaging_backends << Backend::Packaging::Baguette.new
|
||||||
|
@packaging_backends << Backend::Packaging::APK.new
|
||||||
|
@packaging_backends << Backend::Packaging::Pkgutils.new
|
||||||
|
|
||||||
@selected_packaging_backend = @packaging_backends[0]
|
@selected_packaging_backend = @packaging_backends[0]
|
||||||
|
|
||||||
@building_backends << Backend::Building.new "configure", "autotools" do |context, recipe|
|
# Add building backends: configuration, build and install.
|
||||||
next BuildStatus::Pass unless Dir.exists? recipe.dirname
|
@building_backends << Backend::Configure.autotools
|
||||||
|
@building_backends << Backend::Configure.cmake
|
||||||
|
@building_backends << Backend::Build.make
|
||||||
|
@building_backends << Backend::Install.make
|
||||||
|
|
||||||
Dir.cd recipe.dirname
|
# Split = new package from a recipe, each one with different files being included.
|
||||||
|
@splitter_backends << Backend::Splitter.man @prefixes
|
||||||
unless File.exists? "configure"
|
@splitter_backends << Backend::Splitter.dev @prefixes
|
||||||
next BuildStatus::Pass
|
@splitter_backends << Backend::Splitter.src @prefixes
|
||||||
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
|
end
|
||||||
|
|
||||||
def packaging_backend=(name : String)
|
def packaging_backend=(name : String)
|
||||||
@ -190,84 +64,8 @@ class Package::Context
|
|||||||
@selected_packaging_backend = backend
|
@selected_packaging_backend = backend
|
||||||
end
|
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
|
def package(package : Package) : Bool
|
||||||
@selected_packaging_backend.package self, package
|
@selected_packaging_backend.package @packages_directory, @architecture, package
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_recipe(filename : String)
|
def read_recipe(filename : String)
|
||||||
@ -320,8 +118,8 @@ class Package::Context
|
|||||||
end
|
end
|
||||||
|
|
||||||
key, value = match
|
key, value = match
|
||||||
@environment[key] = value
|
Do.environment[key] = value
|
||||||
if @verbosity > 0
|
if Baguette::Context.verbosity > 2
|
||||||
STDOUT.puts "environment: #{key} => #{value}"
|
STDOUT.puts "environment: #{key} => #{value}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
66
src/do.cr
66
src/do.cr
@ -1,9 +1,75 @@
|
|||||||
|
|
||||||
|
class Baguette::Context
|
||||||
|
#class_property logfile_path = "#{@working_directory}/#{@recipe.not_nil!.working_uuid}.log"
|
||||||
|
class_property logfile_path = "/tmp/package-create.log"
|
||||||
|
class_property no_logfile = false
|
||||||
|
end
|
||||||
|
|
||||||
class Do < Process
|
class Do < Process
|
||||||
|
class_property environment = {} of String => String
|
||||||
|
|
||||||
def self.require_cmd(cmd : String)
|
def self.require_cmd(cmd : String)
|
||||||
unless Process.run("which", [ cmd ]).success?
|
unless Process.run("which", [ cmd ]).success?
|
||||||
STDERR.puts "#{cmd} isn't installed"
|
STDERR.puts "#{cmd} isn't installed"
|
||||||
exit 1
|
exit 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.run(chdir, command, args)
|
||||||
|
output = Process::Redirect::Inherit
|
||||||
|
|
||||||
|
if Baguette::Context.no_logfile
|
||||||
|
output = Process::Redirect::Close
|
||||||
|
else
|
||||||
|
# log sub-commands outputs
|
||||||
|
STDOUT.puts "logging command " +
|
||||||
|
"#{command} #{args}".colorize(:light_magenta).to_s
|
||||||
|
STDOUT.puts "in " + Baguette::Context.logfile_path.colorize(:blue).mode(:bright).to_s
|
||||||
|
|
||||||
|
File.open Baguette::Context.logfile_path, "a" do |file|
|
||||||
|
file.puts ""
|
||||||
|
file.puts ""
|
||||||
|
file.puts "logging command $ #{command}"
|
||||||
|
file.puts " parameters $ #{args}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
c = Process.run command, args, chdir: chdir, output: output, error: output, env: @@environment
|
||||||
|
|
||||||
|
case output
|
||||||
|
when File
|
||||||
|
output.close
|
||||||
|
end
|
||||||
|
|
||||||
|
c
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.run(command, args)
|
||||||
|
self.run nil, command, args
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.run(command)
|
||||||
|
self.run nil, command, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.sh(command)
|
||||||
|
self.run nil, "sh", ["-x", "-e", "-c", command]
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.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 self.mv(f1 : String, f2 : String)
|
||||||
|
self.run "mv", [ f1, f2 ]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Log directory creations during splits.
|
||||||
|
def self.mkdir_p(dir : String)
|
||||||
|
self.run "mkdir", [ "-p", dir ]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -28,7 +28,7 @@ class Package::Instructions
|
|||||||
def run(context : Context, recipe : Recipe) : BuildStatus
|
def run(context : Context, recipe : Recipe) : BuildStatus
|
||||||
if size > 0
|
if size > 0
|
||||||
each do |command|
|
each do |command|
|
||||||
child = context.run recipe.building_directory, "sh", ["-x", "-c", command]
|
child = Do.run recipe.building_directory, "sh", ["-x", "-c", command]
|
||||||
|
|
||||||
if child.exit_status != 0
|
if child.exit_status != 0
|
||||||
return BuildStatus::Failed
|
return BuildStatus::Failed
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
require "option_parser"
|
require "option_parser"
|
||||||
require "file_utils"
|
require "file_utils"
|
||||||
require "colorize"
|
require "colorize"
|
||||||
|
require "baguette-crystal-base"
|
||||||
|
|
||||||
require "./context.cr"
|
require "./context.cr"
|
||||||
require "./recipe.cr"
|
require "./recipe.cr"
|
||||||
@ -70,11 +71,11 @@ OptionParser.parse do |parser|
|
|||||||
}
|
}
|
||||||
|
|
||||||
parser.on("-v", "--verbose", "Runs more verbosely.") {
|
parser.on("-v", "--verbose", "Runs more verbosely.") {
|
||||||
context.verbosity += 1
|
Baguette::Context.verbosity += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
parser.on("-q", "--quiet", "Runs more quietely.") {
|
parser.on("-q", "--quiet", "Runs more quietely.") {
|
||||||
context.verbosity -= 1
|
Baguette::Context.verbosity -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
parser.on("-n", "--ignore-dependencies", "Do not try to install build-dependencies.") {
|
parser.on("-n", "--ignore-dependencies", "Do not try to install build-dependencies.") {
|
||||||
@ -172,7 +173,7 @@ begin
|
|||||||
recipes.each do |recipe|
|
recipes.each do |recipe|
|
||||||
latest_build_dir = recipe.building_directory
|
latest_build_dir = recipe.building_directory
|
||||||
|
|
||||||
context.title recipe.name
|
Baguette::Log.title recipe.name
|
||||||
|
|
||||||
recipe.download
|
recipe.download
|
||||||
|
|
||||||
|
@ -216,13 +216,13 @@ class Package::Recipe
|
|||||||
|
|
||||||
unless File.exists? filename
|
unless File.exists? filename
|
||||||
if url.scheme == "file"
|
if url.scheme == "file"
|
||||||
@context.info "Copying '#{url.filename}'"
|
Baguette::Log.info "Copying '#{url.filename}'"
|
||||||
|
|
||||||
FileUtils.cp "#{recipe_directory}/#{url.filename}", filename
|
FileUtils.cp "#{recipe_directory}/#{url.filename}", filename
|
||||||
else
|
else
|
||||||
@context.info "Downloading '#{url.filename}'"
|
Baguette::Log.info "Downloading '#{url.filename}'"
|
||||||
|
|
||||||
status = @context.run @context.sources_directory, "wget", [ url.to_s, "-O", filename ]
|
status = Do.run @context.sources_directory, "wget", [ url.to_s, "-O", filename ]
|
||||||
|
|
||||||
raise DownloadError.new self, url unless status.success?
|
raise DownloadError.new self, url unless status.success?
|
||||||
end
|
end
|
||||||
@ -231,15 +231,15 @@ class Package::Recipe
|
|||||||
end
|
end
|
||||||
|
|
||||||
def extract
|
def extract
|
||||||
@context.mkdir_p building_directory
|
Do.mkdir_p building_directory
|
||||||
|
|
||||||
sources.each do |url|
|
sources.each do |url|
|
||||||
basename = url.filename
|
basename = url.filename
|
||||||
|
|
||||||
if basename.match /\.(tar\.(gz|xz|bz2|lzma)|tgz)$/
|
if basename.match /\.(tar\.(gz|xz|bz2|lzma)|tgz)$/
|
||||||
@context.info "Extracting '#{url.filename}'"
|
Baguette::Log.info "Extracting '#{url.filename}'"
|
||||||
|
|
||||||
status = @context.run(
|
status = Do.run(
|
||||||
building_directory,
|
building_directory,
|
||||||
"bsdtar", [
|
"bsdtar", [
|
||||||
"xf",
|
"xf",
|
||||||
@ -249,9 +249,9 @@ class Package::Recipe
|
|||||||
|
|
||||||
raise ExtractionError.new self, url unless status.success?
|
raise ExtractionError.new self, url unless status.success?
|
||||||
elsif basename.match /\.patch$/
|
elsif basename.match /\.patch$/
|
||||||
@context.info "Applying '#{url.filename}'"
|
Baguette::Log.info "Applying '#{url.filename}'"
|
||||||
|
|
||||||
status = @context.run(
|
status = Do.run(
|
||||||
"#{building_directory}/#{dirname}",
|
"#{building_directory}/#{dirname}",
|
||||||
"patch", [
|
"patch", [
|
||||||
"-i", "#{@context.sources_directory}/#{url.filename}"
|
"-i", "#{@context.sources_directory}/#{url.filename}"
|
||||||
@ -260,7 +260,7 @@ class Package::Recipe
|
|||||||
|
|
||||||
raise ExtractionError.new self, url unless status.success?
|
raise ExtractionError.new self, url unless status.success?
|
||||||
else
|
else
|
||||||
@context.info "Copying '#{url.filename}'"
|
Baguette::Log.info "Copying '#{url.filename}'"
|
||||||
|
|
||||||
directory = if url.scheme == "file"
|
directory = if url.scheme == "file"
|
||||||
@recipe_directory
|
@recipe_directory
|
||||||
@ -282,7 +282,7 @@ class Package::Recipe
|
|||||||
# goes somehow wrong.
|
# goes somehow wrong.
|
||||||
# - Make things thread-safe. (those ENV[]= calls are definitely not)
|
# - Make things thread-safe. (those ENV[]= calls are definitely not)
|
||||||
def build
|
def build
|
||||||
@context.mkdir_p fake_root_directory
|
Do.mkdir_p fake_root_directory
|
||||||
|
|
||||||
ENV["PKG"] = fake_root_directory
|
ENV["PKG"] = fake_root_directory
|
||||||
|
|
||||||
@ -290,7 +290,7 @@ class Package::Recipe
|
|||||||
old_dir = Dir.current
|
old_dir = Dir.current
|
||||||
|
|
||||||
instructions.to_a.each do |instruction|
|
instructions.to_a.each do |instruction|
|
||||||
@context.info "Building ('#{instruction.phase}' phase)"
|
Baguette::Log.info "Building ('#{instruction.phase}' phase)"
|
||||||
|
|
||||||
if instruction.run(@context, self).failed?
|
if instruction.run(@context, self).failed?
|
||||||
raise BuildError.new self, "Building (#{instruction.phase} phase) failed."
|
raise BuildError.new self, "Building (#{instruction.phase} phase) failed."
|
||||||
@ -312,7 +312,7 @@ class Package::Recipe
|
|||||||
end
|
end
|
||||||
|
|
||||||
private def do_strip
|
private def do_strip
|
||||||
@context.info "Stripping binaries"
|
Baguette::Log.info "Stripping binaries"
|
||||||
|
|
||||||
FileUtils.find_files(fake_root_directory) do |path|
|
FileUtils.find_files(fake_root_directory) do |path|
|
||||||
file_output = `file #{path}`
|
file_output = `file #{path}`
|
||||||
@ -327,7 +327,7 @@ class Package::Recipe
|
|||||||
end
|
end
|
||||||
|
|
||||||
if strip_opt
|
if strip_opt
|
||||||
@context.detail "stripping #{path}"
|
Baguette::Log.detail "stripping #{path}"
|
||||||
|
|
||||||
Process.run "strip", [strip_opt, path]
|
Process.run "strip", [strip_opt, path]
|
||||||
end
|
end
|
||||||
@ -357,7 +357,7 @@ class Package::Recipe
|
|||||||
end
|
end
|
||||||
|
|
||||||
if files_to_split.size > 0
|
if files_to_split.size > 0
|
||||||
@context.info "Splitting " + "'#{package.name}'".colorize(:light_red).underline.to_s
|
Baguette::Log.info "Splitting " + "'#{package.name}'".colorize(:light_red).underline.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
# FIXME: What do we do if those are not on the filesystem?
|
# FIXME: What do we do if those are not on the filesystem?
|
||||||
@ -365,11 +365,11 @@ class Package::Recipe
|
|||||||
origin = "#{fake_root_directory}#{file}"
|
origin = "#{fake_root_directory}#{file}"
|
||||||
destination = "#{package.fake_root_directory}#{file}"
|
destination = "#{package.fake_root_directory}#{file}"
|
||||||
|
|
||||||
@context.detail "Moving '#{file}' to split"
|
Baguette::Log.detail "Moving '#{file}' to split"
|
||||||
|
|
||||||
@context.mkdir_p File.dirname destination
|
Do.mkdir_p File.dirname destination
|
||||||
|
|
||||||
@context.mv(
|
Do.mv(
|
||||||
"#{fake_root_directory}#{file}",
|
"#{fake_root_directory}#{file}",
|
||||||
"#{package.fake_root_directory}#{file}"
|
"#{package.fake_root_directory}#{file}"
|
||||||
)
|
)
|
||||||
@ -394,7 +394,7 @@ class Package::Recipe
|
|||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
@context.info "Assembling " + "'#{package.name}'".colorize(:light_red).underline.to_s
|
Baguette::Log.info "Assembling " + "'#{package.name}'".colorize(:light_red).underline.to_s
|
||||||
|
|
||||||
unless @context.package package
|
unless @context.package package
|
||||||
raise PackagingError.new self, package
|
raise PackagingError.new self, package
|
||||||
@ -408,7 +408,7 @@ class Package::Recipe
|
|||||||
|
|
||||||
def watch
|
def watch
|
||||||
if script = @watch_script
|
if script = @watch_script
|
||||||
status, output = @context.captured_sh script
|
status, output = Do.captured_sh script
|
||||||
|
|
||||||
output = output.to_s
|
output = output.to_s
|
||||||
.gsub(/^[ \t\n]*/, "").gsub(/[ \t\n]*$/, "")
|
.gsub(/^[ \t\n]*/, "").gsub(/[ \t\n]*$/, "")
|
||||||
|
@ -44,7 +44,7 @@ class Action
|
|||||||
|
|
||||||
latest_build_dir = recipe.building_directory
|
latest_build_dir = recipe.building_directory
|
||||||
|
|
||||||
context.title recipe.name
|
Do.title recipe.name
|
||||||
|
|
||||||
# recipe.download
|
# recipe.download
|
||||||
# recipe.extract
|
# recipe.extract
|
||||||
|
Reference in New Issue
Block a user