Browse Source

Begin working on dealing with packages

Nathaniel van Diepen 3 years ago
parent
commit
5be6674a72

+ 1 - 1
Archlinux/PKGBUILD

@@ -10,7 +10,7 @@ pkgdesc="A tool for automating the creation and maintenance of pacman repos"
 arch=('any')
 source=("git+https://eeems.codes/Eeems/pacman-repo.git")
 md5sums=('SKIP')
-depends=('python-pyyaml' 'python-blessings' 'python-psutil')
+depends=('python-pyyaml' 'python-blessings' 'python-psutil' 'pyalpm')
 makedepends=('git' 'nuitka')
 license=('MIT')
 # prepare() {}

+ 5 - 0
etc/pacman-repo.d/repos.d/repo.yml

@@ -1 +1,6 @@
 ---
+packages:
+  - bar
+  - name: yay
+  - name: 3ds-libccd
+    url: "https://eeems.codes/Eeems/3ds-libccd.git"

+ 9 - 0
pacman_repo/__init__.py

@@ -2,6 +2,7 @@ import sys
 
 from .config import Config
 from .util import term
+from .repo import Repo
 
 
 def main(args):
@@ -26,6 +27,14 @@ def main(args):
             print(server)
             print()
 
+        repos = []
+        for repo in config.repos:
+            repo = Repo(repo)
+            repos.append(repo)
+            print("Repo: {}".format(repo.name))
+            for package in repo.packages:
+                print("  {0}: {1}".format(package.name, package.url))
+
     except Exception:
         from traceback import format_exc
         msg = "Error encountered:\n" + format_exc().strip()

+ 19 - 28
pacman_repo/config.py

@@ -21,6 +21,21 @@ class BaseConfig(object):
         return yaml.dump(self._data, default_flow_style=False)
 
 
+class ChildConfig(BaseConfig):
+    def __init__(self, config, path):
+        self.path = path
+        self.config = config
+        self.id = slugify(path, max_length=255)
+        self.name = os.path.splitext(os.path.basename(path))[0]
+        if not os.path.exists(path):
+            raise ConfigException("Repo config file not found: {}".format(path))
+
+        with open(path) as f:
+            self._data = yaml.load(f, Loader=yaml.SafeLoader) or {}
+
+        self._data['name'] = self.name
+
+
 class Config(BaseConfig):
     def __init__(self, path):
         self.path = os.path.join(path, 'pacman-repo.yml')
@@ -46,33 +61,9 @@ class Config(BaseConfig):
                     self.servers.append(ServerConfig(self, path))
 
 
-class RepoConfig(BaseConfig):
-    def __init__(self, config, path):
-        self.path = path
-        self.config = config
-        self.id = slugify(path, max_length=255)
-        self.name = os.path.splitext(os.path.basename(path))[0]
-
-        if not os.path.exists(path):
-            raise ConfigException("Repo config file not found: {}".format(path))
-
-        with open(path) as f:
-            self._data = yaml.load(f, Loader=yaml.SafeLoader) or {}
-
-        self._data['name'] = self.name
-
-
-class ServerConfig(BaseConfig):
-    def __init__(self, config, path):
-        self.path = path
-        self.config = config
-        self.id = slugify(path, max_length=255)
-        self.name = os.path.splitext(os.path.basename(path))[0]
-
-        if not os.path.exists(path):
-            raise ConfigException("Repo config file not found: {}".format(path))
+class RepoConfig(ChildConfig):
+    pass
 
-        with open(path) as f:
-            self._data = yaml.load(f, Loader=yaml.SafeLoader) or {}
 
-        self._data['name'] = self.name
+class ServerConfig(ChildConfig):
+    pass

+ 139 - 0
pacman_repo/pkgbuild.py

@@ -0,0 +1,139 @@
+#!/usr/bin/env python
+import os
+import subprocess
+import re
+import tarfile
+import tempfile
+from UserDict import UserDict
+
+
+class InvalidPackage(Exception):
+    pass
+
+
+class PKGBUILD(UserDict):
+    """Representation of an Archlinux package"""
+
+    def __init__(self, file):
+        UserDict.__init__(self)
+        self._required_fields = (
+            'name', 'description', 'version', 'release',
+            'licenses', 'arch',
+        )
+        self._validated = False
+        self._is_valid = False
+        self._errors = []
+        self._warnings = []
+
+        self.load(file)
+
+    def load(self, file):
+        """Parse a PKGBUILD (can be within a tar file) and import the variables"""
+        if not os.path.exists(file):
+            raise Exception("file does not exist")
+        script_dir = os.path.dirname(__file__)
+        if not script_dir:
+            script_dir = os.path.abspath(script_dir)
+        is_temporary = False
+
+        # Check if it's a tarballed PKGBUILD and extract it
+        try:
+            tar = tarfile.open(file, "r")
+        except:
+            if os.path.basename(file) != "PKGBUILD":
+                raise
+        else:
+            to_extract = None
+            for member in tar.getnames():
+                if member.find("PKGBUILD") >= 0:
+                    to_extract = member
+                    break
+            if not to_extract:
+                raise InvalidPackage('tar file does not contain a PKGBUILD')
+            # Create a temporary directory and extract to it
+            directory = tempfile.mkdtemp()
+            tar.extract(to_extract, directory)
+            file = os.path.join(directory, to_extract)
+            is_temporary = True
+
+        # Find the current directory and filename
+        working_dir = os.path.dirname(file)
+        if working_dir == '':
+            working_dir = None
+        filename = os.path.basename(file)
+
+        # Let's parse the PKGBUILD
+        process = subprocess.Popen([
+            os.path.join(script_dir, 'parsepkgbuild.sh'), filename],
+            stdout=subprocess.PIPE, cwd=working_dir)
+        process.wait()
+
+        # "Import" variables into local namespace
+        for expression in process.stdout.readlines():
+            exec 'temp = dict(' + expression.rstrip() + ')'
+            self.update(temp)
+
+        # Remove the temporary file since we don't need it
+        if is_temporary:
+            for root, dirs, files in os.walk(directory, topdown=False):
+                for name in files:
+                    os.remove(os.path.join(root, name))
+                for name in dirs:
+                    os.rmdir(os.path.join(root, name))
+            os.rmdir(directory)
+
+    def validate(self):
+        """Validate PKGBUILD for missing or invalid fields"""
+        # Search for missing fields
+        for field in self._required_fields:
+            if not self[field]:
+                self._errors.append('%s field is required' % field)
+        if not re.compile("^[\w\d_-]+$").match(self['name']):
+            self._errors.append('package name must be alphanumeric')
+        elif not re.compile("[a-z\d_-]+").match(self['name']):
+            self._warnings.append('package name should be in lower case')
+        if self['version'].find('-') >= 0:
+            self._errors.append('version field is not allowed to contain hyphens')
+        if str(self['release']).find('-') >= 0:
+            self._errors.append('release field is not allowed to contain hyphens')
+        # Description isn't supposed to be longer than 80 characters
+        if self['description'] and len(self['description']) > 80:
+            self._warnings.append('description should not exceed 80 characters')
+        # Make sure the number of sources and checksums is the same
+        found_sums = False
+        for checksum in ('md5sums', 'sha1sums', 'sha256sums', 'sha384sums',
+                         'sha512sums'):
+            if self[checksum]:
+                found_sums = True
+                if len(self[checksum]) != len(self['source']):
+                    self._errors.append('amount of %s and sources does not match'
+                            % checksum)
+        if self['source'] and not found_sums:
+            self._errors.append('sources exist without checksums')
+        # Set some variables to quickly determine whether the package is valid
+        self._validated = True
+        if not self.has_errors():
+            self._is_valid = True
+
+    def is_valid(self):
+        """If Package wasn't validated already, validate and report whether
+        package is valid"""
+        if not self._validated:
+            self.validate()
+        return self._is_valid
+
+    def has_errors(self):
+        """Determine whether package has any errors"""
+        return len(self._errors) > 0
+
+    def has_warnings(self):
+        """Determin whether the package has any warnings"""
+        return len(self._warnings) > 0
+
+    def get_errors(self):
+        """Retrieve a list of errors"""
+        return self._errors
+
+    def get_warnings(self):
+        """Retrieve a list of warnings"""
+        return self._warnings

+ 21 - 0
pacman_repo/repo.py

@@ -1,3 +1,24 @@
+class Package(object):
+    def __init__(self, name, url=None):
+        self.name = name
+        self.url = url or "https://aur.archlinux.org/{}.git".format(name)
+
+
 class Repo(object):
     def __init__(self, config):
         self.config = config
+        self.packages = []
+        for package in self.config["packages"]:
+            if isinstance(package, str):
+                self.packages.append(Package(package))
+
+            else:
+                self.packages.append(Package(package["name"], package.get("url")))
+
+    @property
+    def name(self):
+        return self.config.name
+
+    @property
+    def id(self):
+        return self.config.id