Initial commit.
commit
24edb0a159
|
@ -0,0 +1,14 @@
|
|||
name: package
|
||||
version: 0.1.0
|
||||
|
||||
authors:
|
||||
- Luka Vandervelden <lukc@upyum.com>
|
||||
|
||||
# description: |
|
||||
# Short description of package
|
||||
|
||||
dependencies:
|
||||
specparser:
|
||||
git: https://git.karchnu.fr/WeirdOS/recipes-parser
|
||||
|
||||
license: MIT
|
|
@ -0,0 +1,34 @@
|
|||
require "colorize"
|
||||
|
||||
class Weird::Context
|
||||
# FIXME: Use log files.
|
||||
# FIXME: def log(), that puts stuff as-is in the logs.
|
||||
|
||||
def debug(text)
|
||||
STDERR.puts ":: #{text}".colorize(:cyan)
|
||||
STDERR.flush
|
||||
end
|
||||
def info(text)
|
||||
STDOUT
|
||||
.<<(":: ".colorize(:blue))
|
||||
.<<(text.colorize(:white))
|
||||
.<<("\n")
|
||||
STDOUT.flush
|
||||
end
|
||||
def title(text)
|
||||
STDOUT
|
||||
.<<("|> ".colorize(:blue).bright)
|
||||
.<<(text.colorize(:white).bright)
|
||||
.<<("\n")
|
||||
STDOUT.flush
|
||||
end
|
||||
def warning(text)
|
||||
STDERR.puts ":: #{text}".colorize(:yellow)
|
||||
STDERR.flush
|
||||
end
|
||||
def error(text)
|
||||
STDERR.puts "!! #{text}".colorize(:red)
|
||||
STDERR.flush
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
require "option_parser"
|
||||
|
||||
require "./package/*"
|
||||
|
||||
root : String? = nil
|
||||
args = [] of String
|
||||
|
||||
OptionParser.parse do |parser|
|
||||
parser.banner = "usage: package [options] <command> [arguments]"
|
||||
|
||||
parser.unknown_args do |unknown_args|
|
||||
args = unknown_args
|
||||
end
|
||||
end
|
||||
|
||||
context = Package::Context.new
|
||||
|
||||
context.root = root unless root.nil?
|
||||
|
||||
command = args[0]
|
||||
args.shift
|
||||
|
||||
begin
|
||||
case command
|
||||
when "add"
|
||||
context.install args
|
||||
when "remove"
|
||||
context.remove args.map { |s| Package::Atom.from_string s }
|
||||
end
|
||||
rescue e : Package::CollisionException
|
||||
context.error e
|
||||
|
||||
e.collisions.each do |file|
|
||||
context.error " - #{file}"
|
||||
end
|
||||
rescue e : Package::Exception
|
||||
context.error e
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
class Package::Atom
|
||||
# 'name', 'name >= 3.1', 'name:slot', 'name:slot < 3.3.3.3', …
|
||||
getter name : String
|
||||
|
||||
# FIXME: Group these together.
|
||||
getter operator : String?
|
||||
getter version : String?
|
||||
|
||||
getter release : Int32?
|
||||
|
||||
getter slot : String?
|
||||
|
||||
def initialize(@name, @slot = nil)
|
||||
end
|
||||
|
||||
def sanitized_slot
|
||||
slot.try &.gsub /\//, ":"
|
||||
end
|
||||
|
||||
# FIXME: What if other data are provided?
|
||||
def to_s
|
||||
"#{name} :#{slot}"
|
||||
end
|
||||
|
||||
# FIXME: Parse slot syntax, parse operators.
|
||||
def self.from_string(s : String)
|
||||
Atom.new s
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
require "colorize"
|
||||
require "uuid"
|
||||
require "file_utils"
|
||||
|
||||
require "../io.cr"
|
||||
|
||||
require "./weird.cr"
|
||||
|
||||
class Package::Context < Weird::Context
|
||||
property root = "/"
|
||||
|
||||
# Stores informations about already installed packages, their
|
||||
# manifests, and so on.
|
||||
@database = Database.new
|
||||
|
||||
def initialize()
|
||||
end
|
||||
|
||||
def root=(@root)
|
||||
@database.root = "#{@root}#{Database::PATH_FROM_ROOT}"
|
||||
end
|
||||
|
||||
def install(package : Package, data_directory : String)
|
||||
end
|
||||
|
||||
# FIXME: Should atomic upgrades be handled here?
|
||||
def install(files : Array(String))
|
||||
work_dirs = Hash(String, String).new
|
||||
manifests = Hash(String, Manifest).new
|
||||
packages = Hash(String, Package).new
|
||||
|
||||
files.each do |file|
|
||||
work_dir = get_work_directory
|
||||
data_dir = "#{work_dir}/data"
|
||||
|
||||
work_dirs[file] = work_dir
|
||||
|
||||
Weird.extract file, work_dir, data_dir
|
||||
|
||||
package = Package.new "#{work_dir}/control.spec"
|
||||
packages[file] = package
|
||||
|
||||
if is_installed? package.to_atom
|
||||
raise ::Package::Exception.new "Package '#{package.to_atom.to_s}' is already installed."
|
||||
end
|
||||
|
||||
Manifest.new("#{work_dir}/manifest").tap do |manifest|
|
||||
manifest.no_collisions! self
|
||||
|
||||
manifests[file] = manifest
|
||||
end
|
||||
|
||||
# FIXME: Check signatures (not implemented in packages ATM).
|
||||
end
|
||||
|
||||
|
||||
files.each do |file|
|
||||
info "Installing '#{file}'"
|
||||
|
||||
work_dir = work_dirs[file]
|
||||
data_dir = "#{work_dir}/data"
|
||||
|
||||
package = packages[file]
|
||||
manifest = manifests[file]
|
||||
|
||||
manifest.each do |entry|
|
||||
if entry.type == "directory" && Dir.exists? entry.file
|
||||
next
|
||||
end
|
||||
|
||||
debug "++ #{entry.file}"
|
||||
entry.install data_dir, @root
|
||||
end
|
||||
|
||||
@database << {package, manifest}
|
||||
|
||||
FileUtils.rm_r work_dir
|
||||
end
|
||||
end
|
||||
def install(file : String)
|
||||
install [file]
|
||||
end
|
||||
def install(atoms : Array(Atom))
|
||||
end
|
||||
def install(atom : Atom)
|
||||
install [atom]
|
||||
end
|
||||
def upgrade(atom : Atom)
|
||||
end
|
||||
|
||||
def remove(packages : Array(Package))
|
||||
# First part of this function is about extracting
|
||||
# reverse-dependencies. Once we have them all, we remove
|
||||
# everything one by one.
|
||||
all_packages = @database.get_all_packages
|
||||
packages_to_remove = [] of Package
|
||||
unchecked_packages = Deque(Package).new
|
||||
|
||||
packages.each do |package|
|
||||
unchecked_packages << package
|
||||
end
|
||||
|
||||
while unchecked_packages.size > 0
|
||||
package = unchecked_packages.pop
|
||||
|
||||
packages_to_remove << package
|
||||
|
||||
all_packages
|
||||
.select do |p|
|
||||
p.dependencies.any? do |dependency|
|
||||
# FIXME: Check full atoms and not just names.
|
||||
|
||||
# FIXME: Checking there’s no collision with world files is probably done around here.
|
||||
packages_to_remove.find &.name.==(dependency)
|
||||
end
|
||||
end
|
||||
.each do |p|
|
||||
if unchecked_packages.find &.==(p)
|
||||
next
|
||||
end
|
||||
|
||||
if packages_to_remove.find &.==(p)
|
||||
next
|
||||
end
|
||||
|
||||
unchecked_packages << p
|
||||
end
|
||||
end
|
||||
|
||||
packages_to_remove.reverse.each do |package|
|
||||
manifest = @database.get_manifest package
|
||||
|
||||
manifest.reverse.each do |entry|
|
||||
if entry.type == "directory" && (!Dir.exists?(entry.file) || !Dir.empty?(entry.file))
|
||||
next
|
||||
end
|
||||
|
||||
debug "-- #{entry.file}"
|
||||
entry.remove @root
|
||||
end
|
||||
|
||||
@database.remove package
|
||||
end
|
||||
end
|
||||
def remove(package : Package)
|
||||
remove [package]
|
||||
end
|
||||
def remove(atoms : Array(Atom))
|
||||
remove atoms.map { |atom| @database.get_package atom }
|
||||
end
|
||||
def remove(atom : Atom)
|
||||
remove [atom]
|
||||
end
|
||||
|
||||
def installed_packages : Array(Package)
|
||||
end
|
||||
|
||||
def is_installed?(atom : Atom)
|
||||
@database.is_installed? atom
|
||||
end
|
||||
|
||||
def update_repository_cache
|
||||
end
|
||||
def download_to_cache(atom : Atom, url : String)
|
||||
end
|
||||
|
||||
def database_directory
|
||||
end
|
||||
|
||||
def get_work_directory
|
||||
directory = "/tmp/package-#{UUID.random}"
|
||||
FileUtils.mkdir_p directory
|
||||
directory
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
require "file_utils"
|
||||
|
||||
require "./package.cr"
|
||||
require "./manifest.cr"
|
||||
|
||||
class Package::Database
|
||||
PATH_FROM_ROOT = "/var/lib/package"
|
||||
|
||||
property root : String
|
||||
|
||||
def initialize
|
||||
@root = Database::PATH_FROM_ROOT
|
||||
end
|
||||
|
||||
def <<(tuple : Tuple(Package, Manifest))
|
||||
package, manifest = tuple
|
||||
|
||||
entry_path = get_entry_path(package)
|
||||
|
||||
FileUtils.mkdir_p entry_path
|
||||
|
||||
FileUtils.cp package.file_path, "#{entry_path}/package.spec"
|
||||
FileUtils.cp manifest.file_path, "#{entry_path}/manifest"
|
||||
end
|
||||
|
||||
def remove(package : Package)
|
||||
FileUtils.rm_r get_entry_path package.to_atom
|
||||
end
|
||||
|
||||
def is_installed?(atom : Atom)
|
||||
Dir.exists? get_entry_path(atom)
|
||||
end
|
||||
|
||||
def get_package(atom : Atom)
|
||||
if atom.slot.nil?
|
||||
atom = Atom.new atom.name,
|
||||
slot: get_sanitized_slots(atom.name)[0]
|
||||
end
|
||||
|
||||
entry_path = get_entry_path(atom)
|
||||
|
||||
Package.new "#{entry_path}/package.spec"
|
||||
end
|
||||
|
||||
def get_all_packages
|
||||
packages = [] of Package
|
||||
|
||||
Dir.children(@root).each do |name|
|
||||
path = "#{@root}/#{name}"
|
||||
|
||||
Dir.children(path).each do |slot|
|
||||
packages << Package.new "#{path}/#{slot}/package.spec"
|
||||
end
|
||||
end
|
||||
|
||||
packages
|
||||
end
|
||||
|
||||
def get_manifest(package : Package)
|
||||
Manifest.new "#{get_entry_path(package)}/manifest"
|
||||
end
|
||||
|
||||
private def get_sanitized_slots(name : String)
|
||||
Dir.children "#{@root}/#{name}"
|
||||
end
|
||||
|
||||
private def get_entry_path(package : Package)
|
||||
get_entry_path package.to_atom
|
||||
end
|
||||
|
||||
private def get_entry_path(atom : Atom)
|
||||
name = atom.name
|
||||
slot = atom.sanitized_slot
|
||||
|
||||
"#{@root}/#{name}/#{slot}"
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
class Package::Exception < Exception
|
||||
end
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
require "./exception.cr"
|
||||
|
||||
class ::Package::InvalidManifest < ::Package::Exception
|
||||
def initialize(@message)
|
||||
end
|
||||
end
|
||||
|
||||
class ::Package::CollisionException < ::Package::Exception
|
||||
getter collisions
|
||||
def initialize(@message, @collisions : Array(String))
|
||||
end
|
||||
end
|
||||
|
||||
# FIXME: split Entry into File, Symlink and Directory
|
||||
record Package::ManifestEntry, file : String, type : String, data : String? do
|
||||
def install(origin_root, destination_root)
|
||||
origin = "#{origin_root}/#{@file}"
|
||||
destination = "#{destination_root}/#{@file}"
|
||||
|
||||
destination = destination.gsub /\/\.\//, "/"
|
||||
destination = destination.gsub /\/\/*/, "/"
|
||||
|
||||
case @type
|
||||
when "directory"
|
||||
FileUtils.mkdir destination unless Dir.exists? destination
|
||||
when "symlink"
|
||||
link = File.readlink origin
|
||||
File.symlink link, destination
|
||||
when "file", "other"
|
||||
FileUtils.cp origin, destination
|
||||
end
|
||||
end
|
||||
|
||||
def remove(root)
|
||||
path = "#{root}/#{@file}"
|
||||
|
||||
path = path.gsub /\/\.\//, "/"
|
||||
path = path.gsub /\/\/*/, "/"
|
||||
|
||||
case @type
|
||||
when "file", "symlink", "other"
|
||||
FileUtils.rm path
|
||||
when "directory"
|
||||
if Dir.children(path).size == 0
|
||||
FileUtils.rmdir path
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Package::Manifest < Array(Package::ManifestEntry)
|
||||
getter file_path : String
|
||||
|
||||
def initialize(size : Int32, @file_path)
|
||||
initialize size
|
||||
end
|
||||
|
||||
def self.new(file_path) : self
|
||||
lines = File.read(file_path).lines
|
||||
instance = self.new lines.size, file_path
|
||||
|
||||
lines.each do |line|
|
||||
line = line.split ':'
|
||||
|
||||
file, type = line
|
||||
data = line[2]?
|
||||
|
||||
instance << ManifestEntry.new file, type, data
|
||||
end
|
||||
|
||||
instance
|
||||
rescue e : Errno
|
||||
raise e
|
||||
rescue e
|
||||
raise InvalidManifest.new "Could not parse Manifest: invalid entries"
|
||||
end
|
||||
|
||||
def list_collisions(root : String)
|
||||
collisions = [] of ManifestEntry
|
||||
|
||||
each do |entry|
|
||||
case entry.type
|
||||
when "file", "symlink", "other"
|
||||
if File.exists? "#{root}/#{entry.file}"
|
||||
collisions << entry
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
collisions
|
||||
end
|
||||
|
||||
def no_collisions!(context)
|
||||
collisions = list_collisions context.root
|
||||
|
||||
if collisions.size > 0
|
||||
raise CollisionException.new "Collisions detected.", collisions.map &.file
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
require "specparser"
|
||||
|
||||
require "./exception.cr"
|
||||
|
||||
class Package::Package
|
||||
getter name : String
|
||||
getter version : String
|
||||
getter release : Int32
|
||||
getter slot : String
|
||||
|
||||
getter dependencies = Array(String).new
|
||||
|
||||
getter file_path : String
|
||||
|
||||
def initialize(@file_path)
|
||||
specs = SpecParser.parse File.read file_path
|
||||
|
||||
name : String? = nil
|
||||
version : String? = nil
|
||||
release : Int32? = nil
|
||||
slot : 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 "slot"
|
||||
slot = value.as_s
|
||||
when "dependencies"
|
||||
@dependencies = value.as_a_or_s
|
||||
end
|
||||
end
|
||||
|
||||
@name = name.not_nil!
|
||||
@version = version.not_nil!
|
||||
@release = release.not_nil!
|
||||
@slot = slot.not_nil!
|
||||
end
|
||||
|
||||
def to_atom
|
||||
Atom.new @name, slot: @slot
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
require "file_utils"
|
||||
|
||||
class Package::Weird
|
||||
def self.extract(file, metadata_destination, data_destination)
|
||||
FileUtils.mkdir_p "#{metadata_destination}"
|
||||
r = Process.run "tar", ["xf", file, "-C", metadata_destination],
|
||||
output: STDOUT, error: STDERR
|
||||
|
||||
raise Exception.new "Invalid package: extraction failed" unless r.success?
|
||||
|
||||
FileUtils.mkdir_p "#{data_destination}"
|
||||
r = Process.run "tar", [
|
||||
"xf", "#{metadata_destination}/data.tar.xz",
|
||||
"-C", data_destination],
|
||||
output: STDOUT, error: STDERR
|
||||
|
||||
raise Exception.new "Invalid package: data.tar.xz extraction failed" unless r.success?
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue