Initial commit.

master
Luka Vandervelden 2019-10-31 21:57:58 +01:00
commit 24edb0a159
10 changed files with 546 additions and 0 deletions

14
shard.yml Normal file
View File

@ -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

34
src/io.cr Normal file
View File

@ -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

39
src/package.cr Normal file
View File

@ -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

31
src/package/atom.cr Normal file
View File

@ -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

176
src/package/context.cr Normal file
View File

@ -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 theres 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

78
src/package/database.cr Normal file
View File

@ -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

4
src/package/exception.cr Normal file
View File

@ -0,0 +1,4 @@
class Package::Exception < Exception
end

101
src/package/manifest.cr Normal file
View File

@ -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

49
src/package/package.cr Normal file
View File

@ -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

20
src/package/weird.cr Normal file
View File

@ -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