summaryrefslogtreecommitdiffstats
path: root/share/extensions/other/inkman/inkman/target.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--share/extensions/other/inkman/inkman/target.py375
1 files changed, 375 insertions, 0 deletions
diff --git a/share/extensions/other/inkman/inkman/target.py b/share/extensions/other/inkman/inkman/target.py
new file mode 100644
index 0000000..dff1211
--- /dev/null
+++ b/share/extensions/other/inkman/inkman/target.py
@@ -0,0 +1,375 @@
+#
+# Copyright (C) 2019 Martin Owens
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+"""
+Target a directory to install resources into.
+"""
+
+import os
+import sys
+import json
+import logging
+from shutil import which
+
+from inkex.inx import InxFile
+from inkex.command import call, CommandNotFound, ProgramRunError
+
+from .archive import Archive, UnrecognizedArchiveFormat
+from .remote import RemoteArchive, LocalFile
+from .utils import INKSCAPE_PROFILE, CACHE_DIR, ExtensionInx
+from .package import PackageItem, OrphanedItem, PythonItem, PythonPackage
+
+
+class BasicTarget(object):
+ """
+ A location where to install something, plus how to search for it.
+ """
+
+ version_specific = False
+
+ def __init__(self, category, label, path, is_search=False, filters=()):
+ self.category = category
+ self.label = label
+ self.path = os.path.join(INKSCAPE_PROFILE, path)
+ self.is_search = is_search
+ self.archive = RemoteArchive(category)
+ self.filters = filters
+ self._installed = None
+
+ def search(self, query, filtered=False):
+ """Search the online archive for things"""
+ for pkg in self.archive.search(query, filtered):
+ pkg.set_installer(self._install)
+ yield pkg
+
+ def is_writable(self):
+ """Can the folder be written to (for installation)"""
+ try:
+ return os.access(self.path, os.W_OK)
+ except IOError:
+ return False
+
+ def _install(self, filename, info):
+ if not info.get("id"):
+ info["id"] = self.generate_id(filename)
+
+ if not info.get("id"):
+ raise ValueError("Id is a required field for packages")
+
+ if filename.endswith(".zip"):
+ location = info["id"]
+ fname = "info.json"
+ info["files"] = list(self.install_zip_files(filename, location))
+ else:
+ location = "pkg"
+ fname = info["id"] + ".json"
+ info["files"] = [self.write_file(filename, os.path.basename(filename))]
+
+ self.write_file(json.dumps(info).encode("utf8"), fname, extra=location)
+
+ return f"Package installed! Remember to restart inkscape to use it!"
+
+ def _uninstall(self, info, json_file):
+ self._installed = None
+ for fname in info.get("files"):
+ self.remove_file(fname)
+ if json_file and os.path.isfile(json_file):
+ self.remove_file(json_file)
+ return True
+
+ def install_zip_files(self, filename, location):
+ """Install the files in the zip filename as non-pip files"""
+ with Archive(filename) as archive:
+ for filename in archive.filenames():
+ yield self.write_file(archive.read(filename), filename, extra=location)
+
+ def generate_id(self, filename):
+ """User submitted zip file, generate an id as best we can"""
+ return filename.replace(".zip", "")
+
+ def list_installed(self, cached=True):
+ """
+ Loops through all the files in the target path and finds all the installed items.
+ """
+ if cached and self._installed:
+ yield from self._installed
+ return
+
+ self._installed = []
+ for item in self._list_installed():
+ self._installed.append(item)
+ yield item
+
+ def _list_installed(self):
+ orphans = OrphanedItem(self.path)
+ for root, subs, files in os.walk(self.path):
+ for fname in files:
+ fpath = os.path.join(root, fname)
+ name = self.unprefix_path(fpath)
+ if fname.endswith(".json"):
+ if os.path.basename(fpath) == "package.json":
+ continue
+ with open(fpath, "rb") as fhl:
+ data = fhl.read()
+ info = json.loads(data)
+ if not PackageItem.is_pkg(info):
+ continue
+
+ item = PackageItem(
+ info, remote=self._remote_or_local_file(root)
+ )
+ item.set_uninstaller(self._uninstall, fpath)
+ yield item
+
+ for pkg_file in item.get_files(missing=True):
+ orphans.remove_file(pkg_file, item)
+ orphans.remove_file(os.path.join(root, pkg_file), item)
+ else:
+ orphans.add_file(name)
+
+ for fname, item in orphans.get_missing():
+ if item is not None:
+ item._missing.append(fname)
+
+ if orphans.get_files(filters=self.filters):
+ yield orphans
+
+ def _remote_or_local_file(self, basedir):
+ # If a json file specifies something that's local, it's "ALWAYS" local to the
+ # json file as a basedir.
+ def _inner(url):
+ if "://" not in url:
+ return LocalFile(basedir, url)
+ return self.archive._remote_file(url)
+
+ return _inner
+
+ def write_file(self, source, source_name=None, extra=None):
+ target = os.path.join(self.path, extra) if extra else self.path
+
+ if isinstance(source, str):
+ if not source_name:
+ source_name = source
+ with open(source, "rb") as fhl:
+ source = fhl.read()
+
+ path = os.path.join(target, source_name)
+ filedir = os.path.dirname(path)
+ if not os.path.isdir(filedir):
+ os.makedirs(filedir)
+
+ # Ignore paths
+ if not os.path.isdir(path):
+ with open(path, "wb") as whl:
+ whl.write(source)
+
+ return self.unprefix_path(path)
+
+ def remove_file(self, filename):
+ """
+ Remove the given file and clean up
+ """
+ if not filename.startswith(self.path):
+ filename = os.path.join(self.path, filename)
+ if os.path.isfile(filename):
+ os.unlink(filename)
+
+ # Recursively clean up directories (if empty)
+ path = os.path.dirname(filename)
+ while path.lstrip("/") != self.path.lstrip("/"):
+ if os.listdir(path):
+ break
+ os.rmdir(path)
+ path = os.path.dirname(path)
+
+ def unprefix_path(self, path):
+ """
+ Removes the prefix of the given path, if it's based in self.path
+ """
+ # Strip just the OS seperator, but what if the files were moved from another OS?
+ return path.replace(self.path, "").lstrip("/").lstrip("\\")
+
+
+class ExtensionsTarget(BasicTarget):
+ """
+ Extra functional target for extensions (pip based)
+ """
+
+ version_specific = True
+
+ def get_pip(self):
+ path = os.path.abspath(os.path.join(self.path, "bin"))
+ return which("pip", path=path + ":" + os.environ["PATH"])
+
+ def _install(self, filename, info):
+ if self.is_pip_package(filename):
+ results = self.pip_install(filename)
+ if results:
+ info["pip"] = True
+ info["id"] = results.strip().split()[-1]
+ fname = info["id"] + ".json"
+ self.write_file(json.dumps(info).encode("utf8"), fname, extra="lib")
+ return (
+ f"Python Package installed! Remember to restart inkscape to use it!"
+ )
+ return f"Failed to install, something is wrong with your setup."
+
+ return super()._install(filename, info)
+
+ def _uninstall(self, info, json_file):
+ self._installed = None
+ if not info.get("pip"):
+ return super()._uninstall(info, json_file)
+ self.pip_uninstall(info["name"])
+ if json_file and os.path.isfile(json_file):
+ self.remove_file(json_file)
+ return True
+
+ def is_pip_package(self, filename):
+ """Return true if this is a detectable pip package"""
+ if filename.endswith(".whl"):
+ return True
+ try:
+ with Archive(filename) as archive:
+ for filename in archive.filenames():
+ if filename.endswith("setup.py"):
+ return True
+ except UnrecognizedArchiveFormat:
+ return False
+ return False
+
+ def pip_install(self, filename):
+ """Install the filename as a pip package"""
+ pip = self.get_pip()
+ if pip is None:
+ logging.error(
+ "This package requires python VirtualEnv which is not available on your system."
+ )
+ return None
+ try:
+ results = call(
+ pip,
+ "install",
+ ("isolated", True),
+ ("disable-pip-version-check", True),
+ ("cache-dir", CACHE_DIR),
+ filename,
+ ).decode("utf8")
+ except ProgramRunError as err:
+ raise
+ return results
+
+ def pip_uninstall(self, name):
+ """Uninstall the given pip package name"""
+ try:
+ results = call(
+ self.get_pip(), "uninstall", ("disable-pip-version-check", True), name
+ ).decode("utf8")
+ except ProgramRunError as err:
+ raise
+ return results
+
+ def generate_id(self, filename):
+ """Extensions have an id internally, try and use it"""
+ try:
+ with Archive(filename) as archive:
+ inxes = [item for item in archive.filenames() if item.endswith(".inx")]
+ if not inxes:
+ raise IOError("Refusing to install extension without inx file!")
+ inx = ExtensionInx(archive.read(inxes[0]).decode("utf-8"))
+ return inx.ident
+ except UnrecognizedArchiveFormat:
+ raise IOError(
+ "Refusing the install extension without inx file (unknown archive)"
+ )
+ except:
+ raise IOError("Refusing the install extension with bad inx file!")
+
+ def _list_installed(self):
+ """
+ Add pip packages to file lists.
+ """
+ orphans = None
+ packages = {}
+ all_deps = set()
+ all_files = set()
+
+ # First collect a list of python packages installed
+ for node in self.get_python_paths():
+ if node.endswith(".dist-info") or node.endswith(".egg-info"):
+ package = PythonPackage(node, self.path)
+ packages[package.name] = package
+ all_files |= set(package.package_files())
+ for dep, _ in package.get_depedencies():
+ all_deps.add(dep)
+
+ # Now return all non pip packaged extensions (from super)
+ for item in super()._list_installed():
+ if item.info.get("pip", False):
+ if self.info.ident not in packages:
+ print(f"Can't find python package: {item.ient}")
+ continue
+
+ pip_pkg = packages[item.ident]
+ item.info["version"] = pip_pkg.version
+
+ if isinstance(item, OrphanedItem):
+ orphans = item
+ else:
+ yield item
+
+ # Remove all orphaned files that were installed by pip packages
+ if orphans is not None:
+ for fn in all_files:
+ orphans.remove_file(fn)
+ if orphans.get_files(filters=self.filters):
+ # Yield if we still have orphans
+ yield orphans
+
+ # Now what to do with all these remaining packages, pretend their installed?
+ for name, package in packages.items():
+ for inx in package.get_inx():
+ item = PythonItem(package)
+ item.set_uninstaller(self._uninstall, None)
+ yield item
+ break
+
+ for dep in all_deps:
+ if dep not in packages:
+ # These packages are often just installed into the system, nothing to say.
+ # XXX But, there is a future where pip could be interigated.
+ # logging.error(f"Missing python depedency: {dep}")
+ continue
+ packages.pop(dep)
+
+ def get_python_paths(self):
+ """Returns paths related to the python packages"""
+ pyver = "python" + sys.version[:3]
+ for varient in [
+ os.path.join(self.path, "lib", pyver, "site-packages"),
+ ]:
+ if os.path.isdir(varient):
+ for subpath in os.listdir(varient):
+ yield os.path.join(varient, subpath)
+
+ def get_package(self, name, version=None):
+ """Test every package in this list if it matches the name and version"""
+ for package in self.iter():
+ found = package.is_package(name, version=version)
+ if found:
+ return package
+ return None