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