commit 24edb0a159ffb206c207c885aab455e3afb8f02e Author: Luka Vandervelden Date: Thu Oct 31 21:57:58 2019 +0100 Initial commit. diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..9726238 --- /dev/null +++ b/shard.yml @@ -0,0 +1,14 @@ +name: package +version: 0.1.0 + +authors: + - Luka Vandervelden + +# description: | +# Short description of package + +dependencies: + specparser: + git: https://git.karchnu.fr/WeirdOS/recipes-parser + +license: MIT diff --git a/src/io.cr b/src/io.cr new file mode 100644 index 0000000..bac30fb --- /dev/null +++ b/src/io.cr @@ -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 + diff --git a/src/package.cr b/src/package.cr new file mode 100644 index 0000000..88ef9d7 --- /dev/null +++ b/src/package.cr @@ -0,0 +1,39 @@ +require "option_parser" + +require "./package/*" + +root : String? = nil +args = [] of String + +OptionParser.parse do |parser| + parser.banner = "usage: package [options] [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 + diff --git a/src/package/atom.cr b/src/package/atom.cr new file mode 100644 index 0000000..169f10a --- /dev/null +++ b/src/package/atom.cr @@ -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 + diff --git a/src/package/context.cr b/src/package/context.cr new file mode 100644 index 0000000..11d30d3 --- /dev/null +++ b/src/package/context.cr @@ -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 + diff --git a/src/package/database.cr b/src/package/database.cr new file mode 100644 index 0000000..cba6330 --- /dev/null +++ b/src/package/database.cr @@ -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 + diff --git a/src/package/exception.cr b/src/package/exception.cr new file mode 100644 index 0000000..60f7df5 --- /dev/null +++ b/src/package/exception.cr @@ -0,0 +1,4 @@ + +class Package::Exception < Exception +end + diff --git a/src/package/manifest.cr b/src/package/manifest.cr new file mode 100644 index 0000000..f252f3c --- /dev/null +++ b/src/package/manifest.cr @@ -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 + diff --git a/src/package/package.cr b/src/package/package.cr new file mode 100644 index 0000000..7141bfb --- /dev/null +++ b/src/package/package.cr @@ -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 + + diff --git a/src/package/weird.cr b/src/package/weird.cr new file mode 100644 index 0000000..7743d99 --- /dev/null +++ b/src/package/weird.cr @@ -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 +