import glob import json import time import os from collections import OrderedDict from loguru import logger from textwrap import dedent from .action import Action from .util import run_script from ..util import is_installed, get_installed_build class InstallAction(Action): def __init__(self, build, script, config, from_binary_archives=False): name = "install" if not from_binary_archives else "install from binary archives" super().__init__(name, build, script, config) self.from_binary_archives = from_binary_archives def _run(self, args): environment = self.config.global_env() tmp_root = environment["TMP_ROOT"] orchestra_root = environment['ORCHESTRA_ROOT'] logger.info("Preparing temporary root directory") self._prepare_tmproot() pre_file_list = self._index_directory(tmp_root + orchestra_root, strip_prefix=tmp_root + orchestra_root) start_time = time.time() if self.from_binary_archives: self._install_from_binary_archives() else: self._install(args.quiet) self._post_install(args.quiet) end_time = time.time() post_file_list = self._index_directory(tmp_root + orchestra_root, strip_prefix=tmp_root + orchestra_root) new_files = [f for f in post_file_list if f not in pre_file_list] archive_name = self.build.binary_archive_filename archive_path = os.path.join(self.environment["BINARY_ARCHIVES"], archive_name) if args.create_binary_archives and not os.path.exists(archive_path): logger.info("Creating binary archive") self._create_binary_archive() if args.no_merge: return self._uninstall_currently_installed_build(args.quiet) logger.info("Merging installation into Orchestra root directory") self._merge(args.quiet) # Write file metadata and index os.makedirs(self.config.installed_component_metadata_dir(), exist_ok=True) metadata = { "component_name": self.build.component.name, "build_name": self.build.name, "install_time": int(end_time - start_time), } with open(self.config.installed_component_metadata_path(self.build.component.name), "w") as f: json.dump(metadata, f) with open(self.config.installed_component_file_list_path(self.build.component.name), "w") as f: f.truncate(0) f.writelines(new_files) def _is_satisfied(self): return is_installed(self.config, self.build.component.name, wanted_build=self.build.name) def _prepare_tmproot(self): script = dedent(""" rm -rf "$TMP_ROOT" mkdir -p "$TMP_ROOT" mkdir -p "${TMP_ROOT}${ORCHESTRA_ROOT}/include" mkdir -p "${TMP_ROOT}${ORCHESTRA_ROOT}/lib64"{,/include,/pkgconfig} test -e "${TMP_ROOT}${ORCHESTRA_ROOT}/lib" || ln -s lib64 "${TMP_ROOT}${ORCHESTRA_ROOT}/lib" test -L "${TMP_ROOT}${ORCHESTRA_ROOT}/lib" mkdir -p "${TMP_ROOT}${ORCHESTRA_ROOT}/bin" mkdir -p "${TMP_ROOT}${ORCHESTRA_ROOT}/usr/"{lib,include} mkdir -p "${TMP_ROOT}${ORCHESTRA_ROOT}/share/"{info,doc,man} touch "${TMP_ROOT}${ORCHESTRA_ROOT}/share/info/dir" mkdir -p "${TMP_ROOT}${ORCHESTRA_ROOT}/libexec" """) run_script(script, environment=self.environment, quiet=True) def _install(self, quiet): logger.info("Executing install script") run_script(self.script, quiet=quiet, environment=self.environment) def _post_install(self, quiet): # TODO: maybe this should be put into the configuration and not in Orchestra itself logger.info("Converting hardlinks to symbolic") self._hard_to_symbolic(quiet) # TODO: maybe this should be put into the configuration and not in Orchestra itself logger.info("Fixing RPATHs") self._fix_rpath(quiet) # TODO: this should be put into the configuration and not in Orchestra itself logger.info("Replacing NDEBUG preprocessor statements") self._replace_ndebug(True, quiet) def _hard_to_symbolic(self, quiet): hard_to_symbolic = """hard-to-symbolic.py "${TMP_ROOT}${ORCHESTRA_ROOT}" """ run_script(hard_to_symbolic, quiet=quiet, environment=self.environment) def _fix_rpath(self, quiet): fix_rpath_script = dedent(f""" cd "$TMP_ROOT$ORCHESTRA_ROOT" # Fix rpath find . -type f -executable | while read EXECUTABLE; do if head -c 4 "$EXECUTABLE" | grep '^.ELF' > /dev/null && file "$EXECUTABLE" | grep x86-64 | grep -E '(shared|dynamic)' > /dev/null; then REPLACE='$'ORIGIN/$(realpath --relative-to="$(dirname "$EXECUTABLE")" ".") echo "Setting rpath of $EXECUTABLE to $REPLACE" elf-replace-dynstr.py "$EXECUTABLE" "$RPATH_PLACEHOLDER" "$REPLACE" / elf-replace-dynstr.py "$EXECUTABLE" "$ORCHESTRA_ROOT" "$REPLACE" / fi done """) run_script(fix_rpath_script, quiet=quiet, environment=self.environment) def _replace_ndebug(self, enable_debugging, quiet): debug, ndebug = ("1", "0") if enable_debugging else ("0", "1") patch_ndebug_script = dedent(rf""" cd "$TMP_ROOT$ORCHESTRA_ROOT" find include/ -name "*.h" \ -exec \ sed -i \ -e 's|^\s*#\s*ifndef\s\+NDEBUG|#if {debug}|' \ -e 's|^\s*#\s*ifdef\s\+NDEBUG|#if {ndebug}|' \ -e 's|^\(\s*#\s*if\s\+.*\)!defined(NDEBUG)|\1{debug}|' \ -e 's|^\(\s*#\s*if\s\+.*\)defined(NDEBUG)|\1{ndebug}|' \ {{}} ';' """) run_script(patch_ndebug_script, quiet=quiet, environment=self.environment) def _uninstall_currently_installed_build(self, quiet): installed_build = get_installed_build(self.build.component.name, self.config) if installed_build is None: return logger.info("Uninstalling previously installed build") uninstall(self.build.component.name, self.config) def _merge(self, quiet): copy_command = f'cp -farl "$TMP_ROOT/$ORCHESTRA_ROOT/." "$ORCHESTRA_ROOT"' run_script(copy_command, quiet=quiet, environment=self.environment) def _create_binary_archive(self): archive_name = self.build.binary_archive_filename script = dedent(f""" mkdir -p "$BINARY_ARCHIVES" cd "$TMP_ROOT$ORCHESTRA_ROOT" tar caf "$BINARY_ARCHIVES/{archive_name}" --owner=0 --group=0 "." """) run_script(script, quiet=True, environment=self.environment) def _install_from_binary_archives(self): archives_dir = self.environment["BINARY_ARCHIVES"] archive_filepath = os.path.join(archives_dir, self.build.binary_archive_filename) if not os.path.exists(archive_filepath): raise Exception("Binary archive not found!") script = dedent(f""" mkdir -p "$TMP_ROOT$ORCHESTRA_ROOT" cd "$TMP_ROOT$ORCHESTRA_ROOT" tar xaf "{archive_filepath}" """) run_script(script, environment=self.environment, quiet=True) @staticmethod def _index_directory(dirpath, strip_prefix=None): paths = list(glob.glob(f"{dirpath}/**", recursive=True)) if strip_prefix: paths = [remove_prefix(p, strip_prefix) for p in paths] return paths @property def environment(self) -> OrderedDict: env = super().environment env["DESTDIR"] = env["TMP_ROOT"] return env def _implicit_dependencies(self): if self.from_binary_archives: return set() else: return {self.build.configure} class InstallAnyBuildAction(Action): def __init__(self, build, config): installed_build_name = get_installed_build(build.component.name, config) if installed_build_name: chosen_build = build.component.builds[installed_build_name] else: chosen_build = build super().__init__("install any", chosen_build, None, config) self._original_build = build def _implicit_dependencies(self): return {self.build.install} def _run(self, args): return def is_satisfied(self, recursively=False, already_checked=None): return self.build.install.is_satisfied(recursively=recursively, already_checked=already_checked) def _is_satisfied(self): raise NotImplementedError("This method should not be called!") @property def name_for_graph(self): if self.build == self._original_build: return f"install {self.build.component.name} (prefer {self._original_build.name})" else: return f"install {self.build.component.name} (prefer {self._original_build.name}, chosen {self.build.name})" @property def name_for_components(self): return f"{self._original_build.component.name}~{self._original_build.name}" def remove_prefix(string, prefix): if string.startswith(prefix): return string[len(prefix):] else: return string[:] def uninstall(component_name, config): index_path = config.installed_component_file_list_path(component_name) with open(index_path) as f: paths = f.readlines() # Ensure depth first visit by reverse-sorting paths.sort(reverse=True) paths = [path.strip() for path in paths] for path in paths: path = path.lstrip("/") path_to_delete = os.path.join(config.global_env()['ORCHESTRA_ROOT'], path) if os.path.isfile(path_to_delete) or os.path.islink(path_to_delete): logger.debug(f"Deleting {path_to_delete}") os.remove(path_to_delete) elif os.path.isdir(path_to_delete): if os.listdir(path_to_delete): logger.debug(f"Not removing directory {path_to_delete} as it is not empty") else: logger.debug(f"Deleting directory {path_to_delete}") os.rmdir(path_to_delete) logger.debug(f"Deleting index file {index_path}") os.remove(index_path)