From 1c7929604b84d982296f082c060fcc305fd9146e Mon Sep 17 00:00:00 2001 From: Mykyta Holubakha Date: Fri, 7 Jul 2017 10:40:11 +0300 Subject: Overhauled patching support dropped patch package source module added --patch option to the install command added a patch command to patch an existing package integrated patch support into the Package class created a MergedPackage module for operations on packages already in the pomu repo added ways to patch an existing package added a base package source module (template), with base classes for package-specific metadata and the source module per se added a list_add utility function implemented and_, and_then, and or in util.result --- pomu/cli.py | 16 +++++++++- pomu/package.py | 18 +++++++---- pomu/repo/repo.py | 42 ++++++++++++++++++++++++- pomu/source/base.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++ pomu/source/file.py | 5 +-- pomu/source/manager.py | 6 ++++ pomu/source/patch.py | 72 ------------------------------------------- pomu/source/portage.py | 5 +-- pomu/util/misc.py | 22 +++++++++++++ pomu/util/pkg.py | 2 +- pomu/util/result.py | 13 ++++++++ pomu/util/str.py | 11 ------- 12 files changed, 200 insertions(+), 96 deletions(-) create mode 100644 pomu/source/base.py delete mode 100644 pomu/source/patch.py create mode 100644 pomu/util/misc.py delete mode 100644 pomu/util/str.py (limited to 'pomu') diff --git a/pomu/cli.py b/pomu/cli.py index 72cec9b..4e7ea7a 100644 --- a/pomu/cli.py +++ b/pomu/cli.py @@ -6,6 +6,7 @@ from os import path from pomu.repo.init import init_plain_repo, init_portage_repo from pomu.repo.repo import portage_repo_path, portage_repos, pomu_active_repo from pomu.source import dispatcher +from pomu.util.pkg import cpv_split from pomu.util.result import ResultException #TODO: global --repo option, (env var?) @@ -71,12 +72,25 @@ def status(): @main.command() @click.argument('package', required=True) +@click.argument('--patch', nargs=-1) + #help='Patches for the package') @needs_repo def install(package): """Install a package""" - res = dispatcher.install_package(pomu_active_repo(), package).expect() + pkg = dispatcher.get_package(package).expect() + pkg.patch(patch) + res = pomu_active_repo().merge(pkg).expect() print(res) +@main.command() +@click.argument('package', required=True) +@click.argument('patch', type=click.Path(exists=True), nargs=-1, required=True) +def patch(package): + """Patch an existing package""" + category, name, *_ = cpv_split(package) + pkg = pomu_active_repo().get_package(name=name, category=category).expect() + pkg.patch(patch).expect() + @main.command() @click.option('--uri', is_flag=True, help='Specify the package to remove by uri, instead of its name') diff --git a/pomu/package.py b/pomu/package.py index 5027569..4a39cd3 100644 --- a/pomu/package.py +++ b/pomu/package.py @@ -13,10 +13,11 @@ import subprocess from patch import PatchSet from pomu.util.fs import strip_prefix +from pomu.util.misc import list_add from pomu.util.result import Result class Package(): - def __init__(self, name, root, backend=None, category=None, version=None, slot='0', d_path=None, files=None, filemap=None): + def __init__(self, name, root, backend=None, category=None, version=None, slot='0', d_path=None, files=None, filemap=None, patches=[]): """ Parameters: backend - specific source module object/class @@ -35,6 +36,7 @@ class Package(): self.version = version self.slot = slot self.filemap = {} + self.patches = patches if d_path is None and files is None and filemap is None: self.d_path = None self.read_path(self.root) @@ -82,14 +84,18 @@ class Package(): copy2(src, dest) except PermissionError: return Result.Err('You do not have enough permissions') - return Result.Ok() + return Result.Ok().and_(self.apply_patches()) + + def patch(self, patch): + list_add(self.patches, patch) - def apply_patches(self, location, patches): - """Applies a sequence of patches at the location""" + def apply_patches(self): + """Applies a sequence of patches at the root (after merging)""" ps = PatchSet() - for p in patches: + for p in self.patches: ps.parse(open(p, 'r')) - ps.apply(root=location) + ps.apply(root=self.root) + return Result.Ok() def gen_manifests(self, dst): """ diff --git a/pomu/repo/repo.py b/pomu/repo/repo.py index f41e0e4..b60f6c1 100644 --- a/pomu/repo/repo.py +++ b/pomu/repo/repo.py @@ -1,7 +1,9 @@ """Subroutines with repositories""" from os import path, rmdir, makedirs +from shutil import copy2 from git import Repo +from patch import PatchSet import portage from pomu.package import Package @@ -64,6 +66,13 @@ class Repository(): f.write('{}/{}\n'.format(wd, fil)) for m in manifests: f.write('{}\n'.format(strip_prefix(m, self.root))) + if package.patches: + patch_dir = path.join(pkgdir, 'patches') + makedirs(patch_dir, exist_ok=True) + with open(path.join(pkgdir, 'PATCH_ORDER'), 'w') as f: + for patch in package.patches: + copy2(patch, patch_dir) + f.write(path.basename(patch) + '\n') if package.backend: with open(path.join(pkgdir, 'BACKEND'), 'w+') as f: f.write('{}\n'.format(package.backend.__name__)) @@ -107,7 +116,13 @@ class Repository(): version = f.readline().strip() with open(path.join(pkgdir, 'FILES'), 'r') as f: files = [x.strip() for x in f] - return Package(name, self.root, backend, category=category, version=version, slot=slot, files=files) + patches=[] + if path.isfile(path.join(pkgdir, 'PATCH_ORDER')): + with open(path.join(pkgdir, 'PATCH_ORDER'), 'r') as f: + patches = [x.strip() for x in f] + pkg = Package(name, self.root, backend, category=category, version=version, slot=slot, files=files, patches=[path.join(pkgdir, 'patches', x) for x in patches]) + pkg.__class__ = MergedPackage + return pkg def get_package(self, name, category=None, slot=None): """Get a package by name, category and slot""" @@ -161,3 +176,28 @@ def pomu_active_repo(no_portage=None, repo_path=None): if repo: return Result.Ok(Repository(portage_repo_path(repo), repo)) return Result.Err('pomu is not initialized') + +class MergedPackage(Package): + def patch(self, patch): + pkgdir = path.join(self.root, 'metadata', 'pomu', self.category, self.name) + if self.slot != '0': + pkgdir = path.join(pkgdir, self.slot) + if isinstance(patch, list): + for x in patch: + self.patch(x) + return Result.Ok() + ps = PatchSet() + ps.parse(open(patch, 'r')) + ps.apply(root=self.root) + self.add_patch(patch) + return Result.Ok() + + def add_patch(self, patch): + pkgdir = path.join(self.root, 'metadata', 'pomu', self.category, self.name) + if self.slot != '0': + pkgdir = path.join(pkgdir, self.slot) + patch_dir = path.join(pkgdir, 'patches') + makedirs(patch_dir, exist_ok=True) + copy2(patch, patch_dir) + with open(path.join(pkgdir, 'PATCH_ORDER'), 'w+') as f: + f.write(path.basename(patch) + '\n') diff --git a/pomu/source/base.py b/pomu/source/base.py new file mode 100644 index 0000000..5d110db --- /dev/null +++ b/pomu/source/base.py @@ -0,0 +1,84 @@ +""" +A base package source module. +A package source module shall provide two classes: a class representing +a package (implementation details of the source, package-specific metadata, +fetching logic etc.), and the source per se: it is responsible for parsing +package specifications and fetching the specified package, as well as +instantiating specific packages from the metadata directory. +""" + +from pomu.source import dispatcher + +class PackageBase(): + """ + This is a base class for storing package-specific metadata. + It shall be subclassed explicitely. + The class is responsible for fetching the package, and reading/writing + the package-specific metadata. + """ + + """The implementation shall provide a name for the package type""" + __name__ = None + + def fetch(self): + """ + A method which is responsible for fetching the package: it should return a Package object (specifying set of files with sufficient metadata), and specify this package object as the backend for the Package object (to store source-specific metadata). + """ + raise NotImplementedError() + + @staticmethod + def from_data_dir(pkgdir): + """ + This method is responsible for instantiating source-specific metadata + from the metadata directory. + It shall return an instance of this class, and take a path to the meta + directory. + """ + raise NotImplementedError() + + def write_meta(self, pkgdir): + """ + This method shall write source-specific metadata to the provided + metadata directory. + """ + raise NotImplementedError() + + def __str__(self): + """ + The implementation shall provide a method to pretty-print + package-specific metadata (displayed in show command). + Example: + return '{}/{}-{} (from {})'.format(self.category, self.name, self.version, self.path) + """ + raise NotImplementedError() + +@dispatcher.source +class BaseSource: + """ + This is the base package source class. + It should be decorated with @dispatcher.source. + The implementation shall provide methods to parse the package specification, + which would be called by the dispatcher (see manager.py for details). + Parser methods shall be decorated with @dispatcher.handler(priority=...) + decorator (default is 5). + It shall provide a method to instantiate a package of this type from the + metadata directory. + """ + @dispatcher.handler() + def parse_full(uri): + """ + This method shall parse a full package specification (which starts with + the backend name, followed by a colon). + It shall return a package, wrapped in Result. + """ + raise NotImplementedError() + + @classmethod + def from_meta_dir(cls, metadir): + """ + This method is responsible for instantiating package-specific metadata + from the metadata directory. + Example: + return PackageBase.from_data_dir(metadir) + """ + raise NotImplementedError() diff --git a/pomu/source/file.py b/pomu/source/file.py index f42474d..04624e3 100644 --- a/pomu/source/file.py +++ b/pomu/source/file.py @@ -6,11 +6,12 @@ from os import path from pomu.package import Package from pomu.source import dispatcher +from pomu.source.base import PackageBase, BaseSource from pomu.util.pkg import cpv_split, ver_str from pomu.util.query import query from pomu.util.result import Result -class LocalEbuild(): +class LocalEbuild(PackageBase): """A class to represent a local ebuild""" __name__ = 'fs' @@ -43,7 +44,7 @@ class LocalEbuild(): return '{}/{}-{} (from {})'.format(self.category, self.name, self.version, self.path) @dispatcher.source -class LocalEbuildSource(): +class LocalEbuildSource(BaseSource): """The source module responsible for importing local ebuilds""" @dispatcher.handler(priority=5) def parse_ebuild_path(uri): diff --git a/pomu/source/manager.py b/pomu/source/manager.py index 4989b12..61ed9be 100644 --- a/pomu/source/manager.py +++ b/pomu/source/manager.py @@ -42,6 +42,12 @@ class PackageDispatcher(): It would register all the methods of the class marked by @handler with the dispatcher. """ + try: + from pomu.source.base import BaseSource + except ImportError: #circular import + return cls + if cls == BaseSource: + return cls self.backends[cls.__name__] = cls for m, obj in inspect.getmembers(cls): if isinstance(obj, self.handler._handler): diff --git a/pomu/source/patch.py b/pomu/source/patch.py deleted file mode 100644 index 69a0821..0000000 --- a/pomu/source/patch.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -A package source module to import packages from filesystem locations (ebuilds) -""" - -from os import path, mkdtemp - -from pomu.package import Package -from pomu.source import dispatcher -from pomu.util.pkg import cpv_split, ver_str -from pomu.util.query import query -from pomu.util.result import Result - -class Patcher(): - """A class to represent a local ebuild""" - __name__ = 'patch' - - def __init__(self, wrapped, *patches): - self.patches = patches - self.wrapped = wrapped - # nested patching - if wrapped is Patcher: - self.patches = wrapped.patches + self.patches - self.wrapped = wrapped.wrapped - - def fetch(self): - pkg = self.wrapped.fetch() - pkg.backend = self - pd = mkdtemp() - pkg.merge_into(pd) - pkg.apply_patches(pd, self.patches) - return Package(pkg.name, pd, self, pkg.category, pkg.version) - - @staticmethod - def from_data_dir(pkgdir): - with open(path.join(pkgdir, 'PATCH_ORIG_BACKEND'), 'r') as f: - wrapped = dispatcher.backends[bname].from_meta_dir(pkgdir) - patches = [path.join(pkgdir, 'patches', x.strip()) - for x in open(path.join(pkgdir, 'PATCH_PATCHES_ORDER'))] - - def write_meta(self, pkgdir): - with open(path.join(pkgdir, 'PATCH_ORIG_BACKEND'), 'w') as f: - f.write('{}\n'.format(self.wrapped.__name__)) - with open(path.join(pkgdir, 'PATCH_PATCHES_ORDER'), 'w') as f: - for p in self.patches: - f.write(path.basename(p) + '\n') - os.makedirs(path.join(pkgdir, 'patches'), exist_ok=True) - for p in self.patches: - shutil.copy2(p, path.join(pkgdir, 'patches')) - # write originals? - - def __str__(self): - return '{}/{}-{} (from {})'.format(self.category, self.name, self.version, self.path) - -@dispatcher.source -class LocalEbuildSource(): - """The source module responsible for importing and patching various ebuilds""" - @dispatcher.handler() - def parse_full(uri): - if not uri.startswith('patch:'): - return Result.Err() - uri = uri[6:] - patchf, _, pks = uri.partition(':') - if not path.isfile(patchf): - return Result.Err('Invalid patch file') - if not pks: - return Result.Err('Package not provided') - pkg = dispatcher.get_package(pks) - return Result.Ok(Patcher(patchf, pkg) - - @classmethod - def from_meta_dir(cls, metadir): - return Patcher.from_data_dir(cls, metadir) diff --git a/pomu/source/portage.py b/pomu/source/portage.py index f4f112c..89eca7e 100644 --- a/pomu/source/portage.py +++ b/pomu/source/portage.py @@ -9,11 +9,12 @@ from portage.versions import vercmp from pomu.package import Package from pomu.repo.repo import portage_repos, portage_repo_path from pomu.source import dispatcher +from pomu.source.base import PackageBase, BaseSource from pomu.util.pkg import cpv_split, ver_str from pomu.util.portage import repo_pkgs from pomu.util.result import Result -class PortagePackage(): +class PortagePackage(PackageBase): """A class to represent a portage package""" __name__ = 'portage' @@ -58,7 +59,7 @@ class PortagePackage(): @dispatcher.source -class PortageSource(): +class PortageSource(BaseSource): """The source module responsible for fetching portage packages""" @dispatcher.handler(priority=5) def parse_spec(uri, repo=None): diff --git a/pomu/util/misc.py b/pomu/util/misc.py new file mode 100644 index 0000000..a6ebb1b --- /dev/null +++ b/pomu/util/misc.py @@ -0,0 +1,22 @@ +"""Miscellaneous utility functions""" + +def list_add(dst, src): + """ + Extends the target list with a scalar, or contents of the given list + """ + if isinstance(src, list): + dst.extend(src) + else: + dst.append(src) + + +def pivot(string, idx, keep_pivot=False): + """ + A function to split a string in two, pivoting at string[idx]. + If keep_pivot is set, the pivot character is included in the second string. + Alternatively, it is omitted. + """ + if keep_pivot: + return (string[:idx], string[idx:]) + else: + return (string[:idx], string[idx+1:]) diff --git a/pomu/util/pkg.py b/pomu/util/pkg.py index 7544e42..56afff6 100644 --- a/pomu/util/pkg.py +++ b/pomu/util/pkg.py @@ -6,7 +6,7 @@ import re from portage.versions import suffix_value -from pomu.util.str import pivot +from pomu.util.misc import pivot suffixes = [x[0] for x in sorted(suffix_value.items(), key=lambda x:x[1])] diff --git a/pomu/util/result.py b/pomu/util/result.py index cd67fa0..d6933d8 100644 --- a/pomu/util/result.py +++ b/pomu/util/result.py @@ -55,6 +55,19 @@ class Result(): return self._val raise ResultException(msg + ': ' + self._val) + def and_(self, rhs): + if not self.is_ok(): + return Result.Err(self.err()) + return rhs + + def and_then(self, f): + return self.map(f) + + def or_(self, rhs): + if self.is_ok(): + return Result.Ok(self.ok()) + return rhs + def __iter__(self): if self._is_val: yield self._val diff --git a/pomu/util/str.py b/pomu/util/str.py deleted file mode 100644 index 11fc514..0000000 --- a/pomu/util/str.py +++ /dev/null @@ -1,11 +0,0 @@ -"""String processing utilities""" -def pivot(string, idx, keep_pivot=False): - """ - A function to split a string in two, pivoting at string[idx]. - If keep_pivot is set, the pivot character is included in the second string. - Alternatively, it is omitted. - """ - if keep_pivot: - return (string[:idx], string[idx:]) - else: - return (string[:idx], string[idx+1:]) -- cgit v1.2.3-18-g5258