566 lines
20 KiB
Python
Executable File
566 lines
20 KiB
Python
Executable File
#!/usr/bin/python2
|
|
import sys
|
|
sys.path.insert(0, '/usr/lib/entropy/libraries')
|
|
sys.path.insert(0, '../libraries')
|
|
|
|
import time
|
|
import os
|
|
import tempfile
|
|
import errno
|
|
import bz2
|
|
|
|
from entropy.i18n import _
|
|
from entropy.output import print_info, blue, teal, brown, darkgreen, purple, \
|
|
print_error, print_warning, TextInterface
|
|
from entropy.exceptions import SystemDatabaseError
|
|
from entropy.client.interfaces.db import GenericRepository
|
|
from entropy.qa import QAInterface
|
|
from entropy.core.settings.base import SystemSettings
|
|
from entropy.exceptions import EntropyException
|
|
|
|
from entropy.const import const_convert_to_rawstring, etpConst
|
|
import entropy.dep
|
|
import entropy.tools
|
|
|
|
class WebinstallGenerator(TextInterface):
|
|
|
|
SHELL_PREAMBLE = const_convert_to_rawstring("""\
|
|
#!/bin/sh
|
|
|
|
function execute_app() {
|
|
equo install "$0" $@
|
|
return ${?}
|
|
}
|
|
|
|
execute_app $@
|
|
exit ${?}
|
|
|
|
"""+ etpConst['databasestarttag'])
|
|
|
|
class CalculationError(EntropyException):
|
|
"""Raised when an error occured while calculating the work queue"""
|
|
|
|
def __init__(self, repository_id, entropy_repository, package_dirs,
|
|
mirror_urls, regenerate = False):
|
|
self._regenerate = regenerate
|
|
self._repo_id = repository_id
|
|
self._repo = entropy_repository
|
|
self._package_dirs = package_dirs
|
|
# this is part of the (unwritten) specification, don't change it!
|
|
self._mirror_urls_str = "\n".join(mirror_urls)
|
|
self._qa = QAInterface()
|
|
|
|
def __copy_data(self, f_obj_source, f_obj_dest):
|
|
while True:
|
|
chunk = f_obj_source.read(16384)
|
|
if not chunk:
|
|
break
|
|
f_obj_dest.write(chunk)
|
|
f_obj_dest.flush()
|
|
|
|
def _calculate_required_actions(self):
|
|
"""
|
|
Compute the list of package identifiers requiring a webinstall package
|
|
generation and collect the expired ones (that would be removed).
|
|
"""
|
|
package_ids = self._repo.listAllPackageIds()
|
|
download_map = dict((x, self._repo.retrieveDownloadURL(x)) for x in
|
|
package_ids)
|
|
|
|
package_dirs_cache = {}
|
|
expired_webinstall_files = set()
|
|
work_queue = []
|
|
max_count = len(package_ids)
|
|
count = 0
|
|
|
|
def _determine_local_package_path(package_id):
|
|
download_path = download_map[package_id]
|
|
download_path_dir, download_file_name = os.path.split(download_path)
|
|
local_package_dir = package_dirs_cache.get(download_path_dir)
|
|
if local_package_dir is None:
|
|
for package_dir in self._package_dirs:
|
|
if package_dir.endswith(download_path_dir):
|
|
local_package_dir = package_dir
|
|
package_dirs_cache[download_path_dir] = package_dir
|
|
break
|
|
if local_package_dir is None:
|
|
self.output("%s: %s" % (
|
|
brown("Cannot find local package directory for"),
|
|
download_path,
|
|
),
|
|
header = teal(" @@ "),
|
|
importance = 1, back = True,
|
|
level = "error"
|
|
)
|
|
raise WebinstallGenerator.CalculationError(
|
|
"cannot find local package dir for: %s" % (download_path,))
|
|
|
|
local_package_path = os.path.join(local_package_dir,
|
|
download_file_name)
|
|
return local_package_path
|
|
|
|
def _determine_local_etp_path(local_package_dir, package_id):
|
|
atom, category, name, version, slot, tag, revision, branch, \
|
|
etpapi = self._repo.getScopeData(package_id)
|
|
version += "%s%s" % (etpConst['entropyrevisionprefix'], revision,)
|
|
local_etp_fn = entropy.dep.create_package_filename(category,
|
|
name, version, tag, ext = etpConst['packagesext_webinstall'])
|
|
local_etp_path = os.path.join(local_package_dir, local_etp_fn)
|
|
return local_etp_path
|
|
|
|
def _validate_etp_creation(local_package_path, local_etp_path, overwrite):
|
|
if self._regenerate:
|
|
# always generate the file in this case
|
|
return True
|
|
if not os.path.isfile(local_package_path) and \
|
|
os.path.isfile(local_etp_path):
|
|
expired_webinstall_files.add(local_etp_path)
|
|
return False
|
|
if not os.path.isfile(local_package_path):
|
|
# local_etp_path does not exist and package file is not
|
|
# available, skip!
|
|
return False
|
|
if not overwrite:
|
|
if os.path.isfile(local_etp_path):
|
|
# already available
|
|
return False
|
|
return True
|
|
|
|
work_queue_cache = set()
|
|
|
|
for package_id in sorted(package_ids, reverse = True):
|
|
count += 1
|
|
download_path = download_map[package_id]
|
|
if (count % 150 == 0) or (count == 0) or (count == max_count):
|
|
self.output("%s: %s" % (purple("scanning"), download_path),
|
|
header = teal(" @@ "),
|
|
count = (count, max_count),
|
|
importance = 0,
|
|
back = True)
|
|
|
|
# don't enqueue the same package id twice
|
|
if package_id in work_queue_cache:
|
|
continue
|
|
|
|
local_package_path = _determine_local_package_path(package_id)
|
|
local_package_dir = os.path.dirname(local_package_path)
|
|
local_etp_path = _determine_local_etp_path(local_package_dir,
|
|
package_id)
|
|
|
|
valid = _validate_etp_creation(local_package_path,
|
|
local_etp_path, False)
|
|
if not valid:
|
|
continue
|
|
|
|
work_queue.append((package_id, local_package_path, local_etp_path))
|
|
work_queue_cache.add(package_id)
|
|
|
|
# also pull in its reverse dependencies
|
|
# this to ensure that packages referencing this packages will
|
|
# have updated metadata.
|
|
# in this case, etp files are going to be overwritten
|
|
revdeps = self._repo.retrieveReverseDependencies(package_id)
|
|
revdeps = [x for x in revdeps if x not in work_queue_cache]
|
|
for rev_pkg_id in revdeps:
|
|
local_package_path = _determine_local_package_path(rev_pkg_id)
|
|
local_package_dir = os.path.dirname(local_package_path)
|
|
local_etp_path = _determine_local_etp_path(local_package_dir,
|
|
rev_pkg_id)
|
|
valid = _validate_etp_creation(local_package_path,
|
|
local_etp_path, True)
|
|
if valid:
|
|
work_queue.append((rev_pkg_id, local_package_path,
|
|
local_etp_path))
|
|
work_queue_cache.add(rev_pkg_id)
|
|
|
|
return work_queue, expired_webinstall_files
|
|
|
|
def _cleanup_expired_files(self, expired_webinstall_files):
|
|
"""
|
|
Cleanup routine that removes expired webinstall package files.
|
|
"""
|
|
for expired_file in sorted(expired_webinstall_files):
|
|
try:
|
|
os.remove(expired_file)
|
|
except OSError as err:
|
|
self.output("%s %s: %s" % (
|
|
teal("cannot remove"),
|
|
path,
|
|
repr(err),
|
|
),
|
|
header = brown(" @@ "),
|
|
level = "warning",
|
|
importance = 1
|
|
)
|
|
|
|
def _prepare_base_package_repository(self):
|
|
"""
|
|
Prepare an empty Entropy Repository that will be used as base
|
|
for embedding package metadata.
|
|
"""
|
|
treeupdates_actions = self._repo.listAllTreeUpdatesActions()
|
|
|
|
# generate empty repository file and re-use it every time
|
|
# this improves the execution a lot
|
|
orig_fd, tmp_repo_orig_path = tempfile.mkstemp(
|
|
suffix="repo-webinst-gen")
|
|
try:
|
|
empty_repo = GenericRepository(
|
|
readOnly = False,
|
|
dbFile = tmp_repo_orig_path,
|
|
name = "empty",
|
|
xcache = False,
|
|
indexing = False,
|
|
skipChecks = True)
|
|
empty_repo.initializeRepository()
|
|
empty_repo.bumpTreeUpdatesActions(treeupdates_actions)
|
|
empty_repo.commit()
|
|
empty_repo.close()
|
|
return tmp_repo_orig_path
|
|
except Exception:
|
|
os.remove(tmp_repo_orig_path)
|
|
raise
|
|
finally:
|
|
os.close(orig_fd)
|
|
|
|
def _generate_webinstall_package(self, base_repository_path,
|
|
package_id, package_path, etp_path, cache_map):
|
|
"""
|
|
Generate a webinstall package for given package_id matched inside
|
|
the working Entropy Repository instance passed at constructor time.
|
|
If no exceptions are raised, the generation went successful.
|
|
The webinstall file generation is atomic.
|
|
"""
|
|
|
|
# handle caching
|
|
cache_map_len_threshold = 800
|
|
cache_map_len = len(cache_map)
|
|
if cache_map_len > cache_map_len_threshold:
|
|
to_remove = cache_map_len_threshold - cache_map_len
|
|
# LRU logic ! yay!
|
|
for key in sorted(cache_map.keys(), key = lambda x: cache_map[x][0]):
|
|
if to_remove < 1:
|
|
break
|
|
to_remove -= 1
|
|
cache_map.pop(key)
|
|
|
|
# WARNING: usage of undocumented feature of
|
|
# get_deep_dependency_list
|
|
# NOTE: how about build deps?
|
|
pkg_match = (package_id, self._repo)
|
|
matches = self._qa.get_deep_dependency_list(None, pkg_match)
|
|
|
|
tmp_fd, tmp_repo_path = tempfile.mkstemp(
|
|
suffix="repo-webinst-gen")
|
|
with os.fdopen(tmp_fd, "wb") as tmp_repo_f:
|
|
with open(base_repository_path, "rb") as tmp_repo_source_f:
|
|
self.__copy_data(tmp_repo_source_f, tmp_repo_f)
|
|
|
|
dest_repo = None
|
|
compressed_tmp_path = None
|
|
compressed_fd = None
|
|
try:
|
|
|
|
repo_arch = self._repo.getSetting("arch")
|
|
atom = self._repo.retrieveAtom(package_id)
|
|
dest_repo = GenericRepository(
|
|
readOnly = False,
|
|
dbFile = tmp_repo_path,
|
|
name = atom,
|
|
xcache = False,
|
|
indexing = False,
|
|
skipChecks = True)
|
|
|
|
deps_pkg_ids = set([pkg_id for pkg_id, _repo in matches])
|
|
deps_pkg_ids.add(package_id)
|
|
for dep_package_id in deps_pkg_ids:
|
|
cached = cache_map.get(dep_package_id)
|
|
if cached is None:
|
|
data = self._repo.getPackageData(dep_package_id,
|
|
get_content = False, get_changelog = False)
|
|
cache_map[dep_package_id] = (time.time(), data)
|
|
else:
|
|
cache_t, data = cached
|
|
|
|
if "original_repository" in data:
|
|
del data['original_repository']
|
|
dest_package_id = dest_repo.addPackage(data,
|
|
revision = data['revision'],
|
|
do_commit = False,
|
|
formatted_content = True)
|
|
|
|
source = etpConst['install_sources']['unknown']
|
|
if dep_package_id == package_id:
|
|
source = etpConst['install_sources']['user']
|
|
# required in order to make mirror URL to be
|
|
# resolved correctly, and, to make only the main
|
|
# package to be pulled in for install.
|
|
dest_repo.storeInstalledPackage(dest_package_id,
|
|
self._repo_id, source = source)
|
|
|
|
dest_repo._setSetting("plain_packages",
|
|
self._mirror_urls_str)
|
|
dest_repo._setSetting("arch", repo_arch)
|
|
dest_repo.commit()
|
|
dest_repo.close()
|
|
dest_repo = None
|
|
|
|
# ready to bzip2
|
|
compressed_fd = None
|
|
compressed_tmp_path = None
|
|
try:
|
|
compressed_fd, compressed_tmp_path = tempfile.mkstemp(
|
|
suffix="repo-webinst-gen")
|
|
entropy.tools.compress_file(tmp_repo_path,
|
|
compressed_tmp_path, bz2.BZ2File)
|
|
try:
|
|
os.rename(compressed_tmp_path, tmp_repo_path)
|
|
except OSError as err:
|
|
if err.errno != errno.EXDEV:
|
|
raise
|
|
shutil.move(compressed_tmp_path, tmp_repo_path)
|
|
finally:
|
|
if compressed_fd is not None:
|
|
os.close(compressed_fd)
|
|
|
|
tmp_etp_path = etp_path + "._etp_work"
|
|
with open(tmp_etp_path, "wb") as etp_f:
|
|
etp_f.write(WebinstallGenerator.SHELL_PREAMBLE)
|
|
with open(tmp_repo_path, "rb") as bin_f:
|
|
while True:
|
|
chunk = bin_f.read(16384)
|
|
if not chunk:
|
|
break
|
|
etp_f.write(chunk)
|
|
bin_f.flush()
|
|
etp_f.flush()
|
|
os.rename(tmp_etp_path, etp_path)
|
|
|
|
finally:
|
|
if compressed_fd is not None:
|
|
try:
|
|
os.close(compressed_fd)
|
|
except OSError:
|
|
pass
|
|
if compressed_tmp_path is not None:
|
|
try:
|
|
os.remove(compressed_tmp_path)
|
|
except (IOError, OSError):
|
|
pass
|
|
if dest_repo is not None:
|
|
dest_repo.close()
|
|
try:
|
|
os.close(tmp_fd)
|
|
except (OSError, IOError):
|
|
pass
|
|
try:
|
|
os.remove(tmp_repo_path)
|
|
except (OSError, IOError):
|
|
pass
|
|
|
|
def sync(self):
|
|
|
|
self.output(purple("Scanning..."),
|
|
header = teal(" @@ "),
|
|
importance = 1, back = True)
|
|
|
|
work_queue, expired_webinstall_files = \
|
|
self._calculate_required_actions()
|
|
|
|
if not work_queue:
|
|
# nothing to do
|
|
self.output(purple("Nothing to do."),
|
|
header = teal(" @@ "),
|
|
importance = 1)
|
|
return True
|
|
|
|
self.output(purple("Generating web-install packages..."),
|
|
header = teal(" @@ "),
|
|
importance = 1)
|
|
|
|
tmp_repo_orig_path = self._prepare_base_package_repository()
|
|
pkg_data_cache = {}
|
|
max_count = len(work_queue)
|
|
count = 0
|
|
try:
|
|
|
|
for package_id, package_path, etp_path in work_queue:
|
|
|
|
count += 1
|
|
atom = self._repo.retrieveAtom(package_id)
|
|
self.output("%s: %s" % (purple("generating for"), atom),
|
|
header = teal(" @@ "),
|
|
count = (count, max_count),
|
|
importance = 0,
|
|
back = True)
|
|
self._generate_webinstall_package(tmp_repo_orig_path,
|
|
package_id, package_path, etp_path, pkg_data_cache)
|
|
self.output("%s: %s" % (purple("generated"), etp_path),
|
|
header = teal(" @@ "),
|
|
count = (count, max_count),
|
|
importance = 0)
|
|
|
|
finally:
|
|
try:
|
|
os.remove(tmp_repo_orig_path)
|
|
except (OSError, IOError):
|
|
pass
|
|
|
|
# help the garbage collector
|
|
for key in list(pkg_data_cache.keys()):
|
|
del pkg_data_cache[key]
|
|
pkg_data_cache.clear()
|
|
del pkg_data_cache
|
|
|
|
self._cleanup_expired_files(expired_webinstall_files)
|
|
|
|
return True
|
|
|
|
def _print_help(args):
|
|
app_name = os.path.basename(sys.argv[0])
|
|
print_info("%s - %s" % (blue(app_name),
|
|
teal(_("Repository web-install packages generator tool")),))
|
|
print_info(" %s:\t%s %s" % (
|
|
purple(_("generate packages")),
|
|
brown(app_name),
|
|
darkgreen("generate [--regen] <repository id> <repository file path> <packages dirs [list]> -- [<mirror urls [list]>]"))
|
|
)
|
|
print_info(" %s = %s" % (
|
|
teal("<packages dirs [list]>"),
|
|
_("dirs where package files are storied for repository"),)
|
|
)
|
|
print_info(" %s = %s" % (
|
|
teal("<mirror urls [list]>"),
|
|
_("list of package mirror urls (not mandatory)"),)
|
|
)
|
|
print_info(" %s = %s" % (
|
|
teal("--regen"),
|
|
_("regenerate all the package files"),)
|
|
)
|
|
print_info(" %s:\t\t%s %s" % (purple(_("this help")), brown(app_name),
|
|
darkgreen("help")))
|
|
if not args:
|
|
return 1
|
|
return 0
|
|
|
|
def _generate(args):
|
|
|
|
regenerate = False
|
|
if "--regen" in args:
|
|
regenerate = True
|
|
args.remove("--regen")
|
|
|
|
if not args:
|
|
print_error(brown(_("Invalid arguments")))
|
|
return 1
|
|
|
|
repository_id = args.pop(0)
|
|
if not entropy.tools.validate_repository_id(repository_id):
|
|
print_error(brown(_("Invalid repository identifier.")))
|
|
return 1
|
|
if not args:
|
|
print_error(brown(_("Invalid Entropy repository file path")))
|
|
return 1
|
|
|
|
entropy_repository_path = args.pop(0)
|
|
entropy_repository_path_dir = os.path.dirname(entropy_repository_path)
|
|
if not (os.path.isdir(entropy_repository_path_dir) and \
|
|
os.access(entropy_repository_path_dir, os.W_OK | os.R_OK)):
|
|
print_error(brown(_("Invalid Entropy repository file path")))
|
|
return 1
|
|
|
|
packages_dirs = []
|
|
mirror_urls = []
|
|
do_packages_dirs = True
|
|
for arg in args:
|
|
if arg == "--":
|
|
do_packages_dirs = False
|
|
continue
|
|
elif do_packages_dirs:
|
|
packages_dirs.append(arg)
|
|
else:
|
|
mirror_urls.append(arg)
|
|
|
|
if not packages_dirs:
|
|
print_error(brown(_("Missing packages directories")))
|
|
return 1
|
|
|
|
if not mirror_urls:
|
|
# coupled with ETP_REPOSITORIES_CONF
|
|
print_warning(brown(_("Using repositories.conf settings for mirrors")))
|
|
sys_set = SystemSettings()
|
|
mirror_urls = sys_set['repositories']['available'].get(
|
|
repository_id, {}).get('plain_packages', [])
|
|
|
|
if not mirror_urls:
|
|
print_error(brown(_("Missing mirror urls")))
|
|
return 1
|
|
|
|
for package_dir in packages_dirs:
|
|
if not (os.path.isdir(package_dir) and \
|
|
os.access(package_dir, os.R_OK | os.W_OK)):
|
|
print_error("%s: %s" % (
|
|
brown(_("Insufficient permissions")),
|
|
package_dir,))
|
|
return 1
|
|
|
|
lock_map = {}
|
|
|
|
# acquire lock
|
|
lock_file = entropy_repository_path + ".webinstall.lock"
|
|
acquired = False
|
|
try:
|
|
|
|
acquired = entropy.tools.acquire_lock(lock_file, lock_map)
|
|
if not acquired:
|
|
print_error(brown(_("Another instance is running.")))
|
|
return 1
|
|
|
|
repo = GenericRepository(
|
|
dbFile = entropy_repository_path,
|
|
name = repository_id,
|
|
indexing = True,
|
|
readOnly = False,
|
|
xcache = True)
|
|
try:
|
|
repo.validate()
|
|
repo.integrity_check()
|
|
except SystemDatabaseError:
|
|
print_error(brown(_("Invalid repository.")))
|
|
return 1
|
|
# force indexing, if user is not in entropy group, indexing is
|
|
# forced to False.
|
|
repo.setIndexing(True)
|
|
repo.createAllIndexes()
|
|
|
|
generator = WebinstallGenerator(repository_id, repo, packages_dirs,
|
|
mirror_urls, regenerate = regenerate)
|
|
sts = generator.sync()
|
|
repo.close()
|
|
if sts:
|
|
return 0
|
|
return 1
|
|
|
|
finally:
|
|
if acquired:
|
|
entropy.tools.release_lock(lock_file, lock_map)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
args_map = {
|
|
'generate': _generate,
|
|
'help': _print_help,
|
|
'__fallback__': _print_help,
|
|
}
|
|
|
|
argv = sys.argv[1:]
|
|
|
|
if not argv:
|
|
argv.append("help")
|
|
|
|
cmd, args = argv[0], argv[1:]
|
|
func = args_map.get(cmd, args_map.get("__fallback__"))
|
|
rc = func(args)
|
|
raise SystemExit(rc)
|