Compare commits

..

1 Commits

Author SHA1 Message Date
2c2cc18146
Update 'README.md' 2023-08-12 16:00:03 +01:00
10 changed files with 83 additions and 403 deletions

View File

@ -1,15 +1,79 @@
![soypak logo](./assets/soypack-logo.svg)
![soypak logo](https://git.closedless.xyz/ClosedLess/soypak/raw/branch/11435da41b/assets/soypack-logo.svg)
# soypak
An additional package manager for managing Debian apps on Linux distros.
An additional package manager for managing Microsoft apps on Linux distros.
This branch is an early prototype to demonstrate the CLI interaction with soypak,
and provides a vision of what is to be created. It is not an early prototype which
can perform package management. With that, there may be typos and other bugs.
## TODO
## Setup & Installation
This branch can be cloned with the following command.
```
git clone --single-branch --branch 11435da41b https://git.closedless.xyz/ClosedLess/soypak.git
cd soypak/
```
- [x] CLI Parser
- [x] Logging
- [ ] Error system
- [ ] Translation system (I18N and PO files)
- [ ] CLI UI
- [ ] Package installer
- [ ] Dependency resolver
- [ ] Package Plug-in Manager
Afterwards, a virtual environment should be created to prevent system-wide pip
installation.
```
python3 -m venv <virtualenv>
```
Then the following pip installation commands can be ran
```
pip3 install -e .
pip3 install clog-x.x.xxx-py3-none-any.whl # where x.x.xxx is the latest version release
```
You can fetch the latest version of CLog from
[here](https://git.closedless.xyz/ClosedLess/clog/releases). Either the `.whl` or `tar.gz` file.
## Using soypak
After setting up soypak, the app can be ran using the Python interpreter
```
python3 soypak/soypak-cli.py help
```
This will print the help message to the terminal and exit. Here, the `help` command
is used explicitly, however the same can be achieved by providing no commands (an
additional error message will also be printed at the top in red).
The following is the output printed
```
Usage: soypak [options] <command> [arguments]
A package manager to manage Microsoft apps.
Where <command> is one of:
help Display the help message of the give command(s), or print this message.
Options:
-h, --help show this help message and exit
--version show the program's version number and exit
Author(s): Ethan Smith-Coss.
Contacts: ethan.sc@closedless.xyz
Thanks for using our software 🍪
Copyright (C) ClosedLess 2023. Licensed under GNU GPLv3+
```
As stated in the printed message, `help` provides the help message of a given command.
With this, we can pass 'help' as an argument to `help`
```
python3 soypak/soypak-cli.py help help
```
This will produce the following output
```
help: Display the help message of the give command(s), or print this message.
Usage: Usage: help [<command1>, <command2>, ..., <commandn>]
If no arguments are specified, the general help message is displayed.
```
Help messages are structured in the following format:
- `<command name>: <synopsis>` - the name of the command and its shortest description
- `<usage>` - how to use the command in question
- `<summary>` - an extended description of the command

View File

@ -4,7 +4,8 @@ import clog
import soypak.cli.parser as parser
log = clog.Logger.get("runtime_logger")
# partial load the logger
log = lambda: clog.Logger.get("runtime_logger")
class CommandHelpStruct:
@ -34,7 +35,7 @@ class Command:
if error:
# Just get the logger for this one statement (:@Note: if more logging occurs, global variable for logger)
log.error("Unrecognised command: %s" % name).withConsole()
log().error("Unrecognised command: %s" % name).withConsole()
Command.die()
return None
@ -59,13 +60,13 @@ def register_command(_cls=None, *, name: str, alias=None, help: CommandHelpStruc
# check if the command has already been given a registry name (only one should exist)
if hasattr(cls, 'name'):
msg = f"Command {cls.__name__!r} has already been registered with the given name: {getattr(cls, 'name')!r}"
log.error(msg)
log().error(msg)
raise Exception(msg)
# check if that registry name is already taken
if name in Command.registry_names:
msg = f"Command {cls.__name__!r} cannot use registry name {name!r} because it is in use."
log.error(msg)
log().error(msg)
raise Exception(msg)
# set the name of the command as given
@ -73,13 +74,13 @@ def register_command(_cls=None, *, name: str, alias=None, help: CommandHelpStruc
# This is deliberate to make aware that `alias` isn't being used so pointless to pass now.
if alias is not None:
log.warn(f"'alias' argument was passed as something other than None. This is intended for future use.")
log().warn(f"'alias' argument was passed as something other than None. This is intended for future use.")
raise Warning("The 'alias' argument is not intended for use. Please exclude for now.")
# ensure that the `help` given is of the correct type
if not isinstance(help, CommandHelpStruct):
msg = f"The argument 'help' must be of type CommandHelpStruct, not {type(help)!r}"
log.error(msg)
log().error(msg)
raise TypeError(msg)
# construct the help message

View File

@ -1,76 +0,0 @@
import sys
import clog
import pathlib
import argparse
import soypak.cli.command as command
import soypak.deps.transaction as transaction
import soypak.deps.bottler as bottler
_log = clog.Logger.get("runtime_logger")
@command.register_command(name="install", help=command.CommandHelpStruct(
synopsis="Install debian binary packages.",
usage="install <package1> [<package2>, ..., <packagen>]",
summary=("Install one or multiple available packages via soypak."))
)
class Install(command.Command):
def __init__(self, *, data: argparse.Namespace, args: list[str] = []) -> None:
super().__init__()
self.data = data
self.rargs = args
def run(self):
candidates: list[bottler.PackageItem] = []
# any preinit before installing should occur here
for item in self.rargs:
# has a bottle file been directly passed?
if item.endswith(".bottle"):
_log.debug("Detected a bottle file. Attempting to sideload...", end="").withConsole()
_log.writeLog(header=False)
# check if the file actually exists on the system
if not pathlib.Path(item).resolve().exists():
_log.printLog("failed.", level=4)
_log.warn(f"Could not find bottle file {item!r}. Skipping package.").withConsole()
continue
# let's structure the file to be processed.
pkg = bottler.loadPackage(item)
if isinstance(pkg, Exception):
_log.printLog("failed.", level=4)
# :@Ethan: maybe the loadPackage could give us some information.
_log.error(f"Unable to sideload bottle file {item!r}. The following error was raised: {str(pkg)}")
sys.exit(1)
_log.printLog("ok.", level=1)
_log.debug(f"Sideloaded: {item}")
candidates.append(pkg)
continue
# let's check the database for a package by the given name
...
if not candidates:
_log.info("Nothing to install.").withConsole()
sys.exit(0)
tx = transaction.Transaction(transaction.PkgGoal.PKG_INSTALL)
depends = tx.compute(candidates)
if len(depends) > 0:
_log.debug("Computed system dependencies required by the packages selected.\n", *depends)
_log.printLog("The following system dependencies have been identified:\n",
*depends, "", level=3, sep=" | ")
tx.apply()
if not tx.problems():
...
# :@TODO: handle all the problems
# commit the transaction to record
tx.commit()

View File

@ -4,8 +4,7 @@ import clog
import soypak.cli.command as command
# import all the commands to auto-register
from soypak.cli.commands import (
help,
install,
help
)
import soypak.cli.parser as parser

View File

@ -1 +0,0 @@
rack_db_dir = "/var/db/soypak/rack"

View File

@ -1,120 +0,0 @@
import re
import sys
import clog
import soypak.util as util
from io import TextIOWrapper
_log = clog.Logger.get("runtime_logger")
_PACKAGE_KEYS = {"Package", "Version", "Maintainer", "Filename", "Package-Repo", "Description", "Section",
"Priority", "Essential", "Architecture", "Origin", "Bugs", "Homepage", "Tag", "Depends",
"Pre-Depends", "Recommends", "Suggests", "Breaks", "Conflicts", "Replaces", "Provides"}
def _parse_bottle_file(item: TextIOWrapper) -> dict[str, str]:
# read the contents directly into memory
contents = item.read()
# first grab the description (if exists), then remove it
desc = list(re.finditer(util.re_desc_tag, contents))
contents = re.sub(util.re_desc_tag, "", contents)
# clean out the comments, strip leading/trailing whitespace, and split the lines
contents = re.sub(util.re_comments, "", contents).strip().splitlines()
parameters = {}
# handle for description
if not desc:
_log.error("Missing field from bottle file, 'Description'.").withConsole()
sys.exit(-1)
# format, clean and add to parameters dictionary
desc = str(desc[0].group()).split("\n")
parameters['Description'] = (desc[0].lstrip("Description:").strip(), "\n".join(desc[1:]))
for param in contents:
# handle for when newlines were split (empty string)
if not param:
continue
# split the key-value and strip it
try:
key, val = map(lambda _: _.strip(), param.split(':'))
except ValueError:
# we should stop processing the file
_log.error("The bottle file contains broken fields.").withConsole()
sys.exit(-1)
if key in parameters:
# The bottle file has a parameter repeated multiple times. This is parsing error
_log.error(f"Parameter {key!r} occurs more than once in bottle file.").withConsole()
sys.exit(-1)
if key not in _PACKAGE_KEYS:
_log.error(f"Parameter {key!r} is not a valid field for bottle files.").withConsole()
sys.exit(-1)
# now we can safely add the key to the dictionary
parameters[key] = val
return parameters
class _Soybottle:
__slots__ = "package", "version", "maintainer", "desc", "package_repo", "filename", "optional"
def __init__(self, *, pkg, version, maintainer, desc, pkg_repo, filename, optionals={}) -> None:
self.package = pkg
self.version = version
self.maintainer = maintainer
self.desc = desc
self.package_repo = pkg_repo
self.filename = filename
self.optional = optionals
def __repr__(self) -> str:
return '{' + ", ".join("{0!r}: {1!r}".format(k, getattr(self, k)) for k in type(self).__slots__) + '}'
@classmethod
def loadPackage(cls, *, fields: dict[str, str]):
REQUIRED = {"Package", "Version", "Maintainer", "Description", "Package-Repo", "Filename"}
try:
for k in REQUIRED:
fields[k]
except KeyError as err:
_log.error("Missing mandatory key from bottle file %s" % str(err)).withConsole()
sys.exit(-1)
# grab all the optional fields from the bottle file
optionals = dict(map(lambda k: (k, fields.pop(k)), REQUIRED ^ _PACKAGE_KEYS))
if len(fields) > 6:
# just log a warning that the bottle file as some extra values unrecognised (not really critical)
_log.warn("Identified additional keys in bottle file that are unrecognised.")
return cls(pkg=fields['Package'], version=fields['Version'], maintainer=['Maintainer'], desc=['Description'],
pkg_repo=fields['Package-Repo'], filename=fields['Filename'], optionals=optionals)
class PackageItem:
def __init__(self, info: _Soybottle) -> None:
self.installed = False
self.pkg_info = info
def loadPackage(item: str) -> PackageItem | Exception:
try:
fp = open(item, 'r')
except (IOError, FileNotFoundError) as err:
return err
_log.debug(f"Obtained file point {fp!r}")
res = _Soybottle.loadPackage(fields=_parse_bottle_file(fp))
# permanently store the bottle file to the database
result = util.rack_bottle(fp, "")
if not result:
_log.error("The bottle file could not be permanently saved to soypak.").withConsole()
sys.exit(-1)
return PackageItem(res)

View File

@ -1,13 +0,0 @@
import clog
import soypak.deps.bottler as bottler
_log = clog.Logger.get("runtime_logger")
def install_packages(tx, pkgs: list[bottler.PackageItem]):
_log.debug(f"The transaction goal is {tx.pkg_goal=}. Attempting to perform installation.")
# :@TODO: install the damned things!
_log.debug("Installation of packages is complete. Ensure to resolve problems within the transaction.")

View File

@ -1,54 +0,0 @@
import enum
import sys
import clog
import soypak.deps.bottler as bottler
import soypak.deps.operations as ops
_log = clog.Logger.get("runtime_logger")
class PkgGoal(enum.Enum):
PKG_INSTALL = 1
class Transaction:
def __init__(self, goal: PkgGoal) -> None:
"""Setup a new transaction."""
if not isinstance(goal, PkgGoal):
_log.error(f"TypeError: {goal=}, expected PkgGoal.")
_log.printLog("There was an issue when operating on the transaction.", level=4)
sys.exit(1)
self.pkg_goal: PkgGoal = goal
def compute(self, pkgs: list[bottler.PackageItem]) -> tuple:
"""Compute dependencies and conflicts of packages. Returns a list of total packages to install."""
self._final_state = []
sys_depends = []
for pkg in pkgs:
# add the package itself to the final state since it can be installed
self._final_state.append(pkg)
# :@TODO: compute the dependencies
return tuple(sys_depends)
def apply(self):
if self.pkg_goal == PkgGoal.PKG_INSTALL:
ops.install_packages(self, self._final_state)
def problems(self) -> tuple:
"""Provide a report of conflicts and packages which cannot be resolved (dependencies not installable)."""
return ()
def commit(self):
"""Commit the transaction to record for what happened."""
pass
@property
def finalised(self):
return self._final_state

View File

@ -1,85 +0,0 @@
import os
import re
import clog
import errno
import soypak.db
from io import TextIOWrapper
_log = clog.Logger.get("runtime_logger")
re_comments = re.compile(r"(--.*$|\*--(\n.*?)+--\*)", re.MULTILINE)
re_desc_tag = re.compile(r"^Description:(?: .*\n)+", re.MULTILINE)
# from DNF util module (https://github.com/rpm-software-management/dnf/blob/master/dnf/util.py#L142)
def ensure_dir(dname) -> tuple[int, str]:
try:
os.makedirs(dname, mode=0o755)
except OSError as err:
if err.errno != errno.EEXIST or not os.path.isdir(dname):
return err.errno, str(err)
return 0, ""
# from eopkg util module (https://github.com/solus-project/package-management/blob/master/pisi/util.py#L293)
def join_path(a, *p) -> str:
"""Join two or more pathname components.
Python os.path.join cannot handle '/' at the start of latter components.
"""
for b in p:
b = b.lstrip('/')
if a == '' or a.endswith('/'):
a += b
continue
a += '/' + b
return a
def rack_bottle(fp: TextIOWrapper | str, f_name: str) -> bool:
"""Store a bottle file in the database and update package cache."""
if isinstance(fp, TextIOWrapper):
_log.debug(f"{fp=}, ensuring file is read from beginning...", end="")
try:
# ensure we are at the start of the file before reading
fp.seek(0)
c = fp.read()
except IOError as err:
_log.writeLog("failed.", header=False)
_log.error(f"Couldn't open file for reading: {str(err)}")
return False
finally:
_log.writeLog("ok.", header=False)
fp.close()
f_name = fp.name.split('/')[-1]
else:
_log.debug("fp is a string. The contents can be directly used.")
c = fp
# clean the file of any comments (specific to storing from user input)
if re.search(re_desc_tag, c):
_log.debug("Scrubbing bottle comments from string content.")
c = re.sub(re_desc_tag, "", c)
result = ensure_dir(soypak.db.rack_db_dir)
if result[0] != 0:
_log.error(f"There was an issue when ensuring directory (exit code: {result[0]}): {result[1]}")
return False
new_file = join_path(soypak.db.rack_db_dir, f_name)
_log.debug(f"Opening new file to write bottle file to permanently: {new_file!r}")
try:
fp = open(new_file, 'w')
except IOError as err:
_log.error(f"There was an issue attempting to write to {new_file!r}: {str(err)}")
return False
fp.write(c)
s = fp.tell()
fp.close()
_log.debug("Successfully wrote %s bytes to file." % s)
return True

View File

@ -1,35 +0,0 @@
-- Mandatory
Package: foo
Version: 0.1
Maintainer: Joe Bloggs
Filename: [uri|path-spec|ask|stdin]
Package-Repo: [url|local]
Description: <short description>
<long description>
-- Optional
Section: <section>
Priority: <priority>
*--
In Debian, the Section and Priority fields have a defined set of accepted values based on the Policy Manual. A list of
these values can be obtained from the latest version of the debian-policy package.
--*
Essential: <yes|no>
Architecture: <arch|all>
Origin: <name>
Bugs: <url>
Homepage: <url>
Tag: <tag list>
Depends: <package list>
Pre-Depends: <package list>
Recommends: <package list>
Suggests: <package list>
Breaks: <package list>
Conflicts: <package list>
Replaces: <package list>
Provides: <package list>
*--
Multiline comments.
ClosedLess (c) 2021-2024
--*