Improve package building

This commit is contained in:
Kivi Kaitaniemi
2024-04-25 01:44:15 +03:00
parent b1be2661e5
commit 352d7db089
4 changed files with 448 additions and 290 deletions
+22 -5
View File
@@ -141,22 +141,39 @@ class Commands:
""" """
return ["less", file] return ["less", file]
def make_chroot(self, chroot_root_dir: str, def make_chroot(self, chroot_dir: str, with_pkgs: list[str]) -> list[str]:
with_pkgs: list[str]) -> list[str]:
""" """
Running this command creates a new arch chroot to the chroot directory and installs the Running this command creates a new arch chroot to the chroot directory and installs the
given packages there. given packages there.
""" """
return ["mkarchroot", chroot_root_dir] + with_pkgs return ["mkarchroot", chroot_dir] + with_pkgs
def make_chroot_pkg(self, chroot_dir: str, user: str, def install_chroot_packages(self, chroot_dir: str, packages: list[str]):
"""
Running this command installs the given packages to the given chroot.
"""
return [
"arch-nspawn", chroot_dir, "pacman", "-S", "--needed",
"--noconfirm"
] + packages
def remove_chroot_packages(self, chroot_dir: str, packages: list[str]):
"""
Running this command removes the given packages from the given chroot.
"""
return ["arch-nspawn", chroot_dir, "pacman", "-Rsu", "--noconfirm"
] + packages
def make_chroot_pkg(self, chroot_wd_dir: str, user: str,
pkgfiles_to_install: list[str]) -> list[str]: pkgfiles_to_install: list[str]) -> list[str]:
""" """
Running this command creates a package file using the given chroot. Running this command creates a package file using the given chroot.
The package is created as the user and the pkg_files_to_install are installed The package is created as the user and the pkg_files_to_install are installed
in the chroot before the package is created. in the chroot before the package is created.
""" """
makechrootpkg_cmd = ["makechrootpkg", "-r", chroot_dir, "-U", user] makechrootpkg_cmd = [
"makechrootpkg", "-c", "-r", chroot_wd_dir, "-U", user
]
for pkgfile in pkgfiles_to_install: for pkgfile in pkgfiles_to_install:
makechrootpkg_cmd += ["-I", pkgfile] makechrootpkg_cmd += ["-I", pkgfile]
+17 -2
View File
@@ -18,6 +18,13 @@ _GRAY_PREFIX = "\033[90m"
_RESET_SUFFIX = "\033[m" _RESET_SUFFIX = "\033[m"
def print_continuation(msg: str):
"""
Prints a message without a prefix.
"""
print(f"{_DECMAN_MSG_TAG}\t {msg}")
def print_error(error_msg: str): def print_error(error_msg: str):
""" """
Prints an error message to the user. Prints an error message to the user.
@@ -218,6 +225,9 @@ class Pacman:
Interface for interacting with pacman. Interface for interacting with pacman.
""" """
def __init__(self):
self._installable = {}
def get_installed(self) -> list[str]: def get_installed(self) -> list[str]:
""" """
Returns a list of installed packages. Returns a list of installed packages.
@@ -239,9 +249,14 @@ class Pacman:
""" """
Returns True if a dependency can be installed using pacman. Returns True if a dependency can be installed using pacman.
""" """
return subprocess.run(conf.commands.is_installable(dep), if dep in self._installable:
return self._installable[dep]
result = subprocess.run(conf.commands.is_installable(dep),
check=False, check=False,
capture_output=True).returncode == 0 capture_output=True).returncode == 0
self._installable[dep] = result
return result
def get_versioned_foreign_packages(self) -> list[tuple[str, str]]: def get_versioned_foreign_packages(self) -> list[tuple[str, str]]:
""" """
@@ -295,7 +310,7 @@ class Pacman:
subprocess.run( subprocess.run(
conf.commands.set_as_explicitly_installed(as_explicit), conf.commands.set_as_explicitly_installed(as_explicit),
check=True, check=True,
capture_output=True) capture_output=conf.quiet_output)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise UserFacingError( raise UserFacingError(
"Failed to install foreign packages.") from error "Failed to install foreign packages.") from error
+368 -248
View File
@@ -2,15 +2,12 @@
Module for interacting with the AUR. Module for interacting with the AUR.
Optional dependencies are ignored when installing AUR packages. Optional dependencies are ignored when installing AUR packages.
Make and check dependencies are grouped together.
Terminology: Terminology:
- package (pkg): name of an package from pacman repos or AUR - package (pkg): name of an package from pacman repos or AUR
- dependency (dep): (virtual) package required when building and running a package - dependency (dep): (virtual) package required when building and running a package
- dependency package (dep pkg): dependency that has been resolved to a package name - dependency package (dep pkg): dependency that has been resolved to a package name
- build dependency: (virtual) package required when building a package (makedepends + checkdepends)
- build dependency package: build dependency that has been resolved to a package name
- all dependencies: normal dependencies and build dependencies combined - all dependencies: normal dependencies and build dependencies combined
""" """
@@ -34,6 +31,24 @@ def strip_dependency(dep: str) -> str:
return rx.sub("", dep) return rx.sub("", dep)
def is_devel(package: str) -> bool:
"""
Returns True if the given package is a devel package.
"""
devel_suffixes = [
"-git",
"-hg",
"-bzr",
"-svn",
"-cvs",
"-darcs",
]
for suffix in devel_suffixes:
if package.endswith(suffix):
return True
return False
class PackageInfo: class PackageInfo:
""" """
Simplified information about an package. Simplified information about an package.
@@ -43,17 +58,41 @@ class PackageInfo:
def __init__(self, pkgname: str, pkgbase: str, version: str, def __init__(self, pkgname: str, pkgbase: str, version: str,
provides: list[str], dependencies: list[str], provides: list[str], dependencies: list[str],
make_and_check_dependencies: list[str], git_url: str): make_dependencies: list[str], check_dependencies: list[str],
git_url: str, pacman: l.Pacman):
self.pkgname = pkgname self.pkgname = pkgname
self.pkgbase = pkgbase self.pkgbase = pkgbase
self.version = version self.version = version
self.dependencies = dependencies
self.build_dependencies = make_and_check_dependencies
self.provides = provides self.provides = provides
self.git_url = git_url self.git_url = git_url
self._aur_deps = None
self._pacman_deps = None self.foreign_dependencies_stripped = []
self._pacman_all_deps = None self.foreign_make_dependencies_stripped = []
self.foreign_check_dependencies_stripped = []
self.pacman_dependencies = []
self.pacman_make_dependencies = []
self.pacman_check_dependencies = []
for dep in dependencies:
if pacman.is_installable(dep):
self.pacman_dependencies.append(dep)
else:
self.foreign_dependencies_stripped.append(
strip_dependency(dep))
for make_dep in make_dependencies:
if pacman.is_installable(make_dep):
self.pacman_make_dependencies.append(make_dep)
else:
self.foreign_make_dependencies_stripped.append(
strip_dependency(make_dep))
for check_dep in check_dependencies:
if pacman.is_installable(check_dep):
self.pacman_check_dependencies.append(check_dep)
else:
self.foreign_check_dependencies_stripped.append(
strip_dependency(check_dep))
def pkg_file_prefix(self) -> str: def pkg_file_prefix(self) -> str:
""" """
@@ -61,71 +100,10 @@ class PackageInfo:
""" """
return f"{self.pkgname}-{self.version}" return f"{self.pkgname}-{self.version}"
def all_foreign_dependencies_stripped(self, pacman: l.Pacman) -> list[str]:
"""
Returs a list of dependencies that cannot be installed from pacman repos.
Includes build dependencies.
Removes version spefications from package names.
"""
if self._aur_deps is not None:
return self._aur_deps
result = []
for p in self.dependencies:
if not pacman.is_installable(p):
result.append(strip_dependency(p))
for p in self.build_dependencies:
if not pacman.is_installable(p):
result.append(strip_dependency(p))
self._aur_deps = result
return result
def all_pacman_dependencies(self, pacman: l.Pacman) -> list[str]:
"""
Returs a list of dependencies that can be installed from pacman repos.
Includes build dependencies.
"""
if self._pacman_all_deps is not None:
return self._pacman_all_deps
result = []
for p in self.dependencies:
if pacman.is_installable(p):
result.append(strip_dependency(p))
for p in self.build_dependencies:
if pacman.is_installable(p):
result.append(strip_dependency(p))
self._pacman_all_deps = result
return result
def pacman_dependencies(self, pacman: l.Pacman) -> list[str]:
"""
Returs a list of dependencies that can be installed from pacman repos.
Doesn't include build dependencies.
"""
if self._pacman_deps is not None:
return self._pacman_deps
result = []
for p in self.dependencies:
if pacman.is_installable(p):
result.append(strip_dependency(p))
self._pacman_deps = result
return result
class ForeignPackage: class ForeignPackage:
""" """
Class used to keep track of AUR/user dependency packages of an AUR/user package. Class used to keep track of AUR/user recursive dependency packages of an AUR/user package.
""" """
def __init__(self, name: str): def __init__(self, name: str):
@@ -202,6 +180,9 @@ class DepGraph:
child_pkgname, DepNode(ForeignPackage(child_pkgname))) child_pkgname, DepNode(ForeignPackage(child_pkgname)))
self.package_nodes[child_pkgname] = child_node self.package_nodes[child_pkgname] = child_node
if len(child_node.children) == 0:
self._childless_node_names.add(child_pkgname)
if parent_pkgname is None: if parent_pkgname is None:
return return
@@ -210,9 +191,8 @@ class DepGraph:
if parent_node.is_pkgname_in_parents_recursive(child_pkgname): if parent_node.is_pkgname_in_parents_recursive(child_pkgname):
raise l.UserFacingError( raise l.UserFacingError(
f"Foreign package dependency cycle detected involving '{child_pkgname}' \ f"Foreign package dependency cycle detected involving '{child_pkgname}' \
and '{parent_pkgname}'. Foreign package dependencies are also required \ and '{parent_pkgname}'. Foreign package dependencies are also required \
during package building and therefore dependency cycles cannot be handled." during package building and therefore dependency cycles cannot be handled.")
)
parent_node.children[child_pkgname] = child_node parent_node.children[child_pkgname] = child_node
child_node.parents[parent_pkgname] = parent_node child_node.parents[parent_pkgname] = parent_node
@@ -220,9 +200,6 @@ class DepGraph:
if parent_pkgname in self._childless_node_names: if parent_pkgname in self._childless_node_names:
self._childless_node_names.remove(parent_pkgname) self._childless_node_names.remove(parent_pkgname)
if len(child_node.children) == 0:
self._childless_node_names.add(child_pkgname)
def get_and_remove_outer_dep_pkgs(self) -> list[ForeignPackage]: def get_and_remove_outer_dep_pkgs(self) -> list[ForeignPackage]:
""" """
Returns all childless nodes of the dependency package graph and removes them. Returns all childless nodes of the dependency package graph and removes them.
@@ -252,7 +229,8 @@ class ExtendedPackageSearch:
Results are cached and user defined packages are preferred. Results are cached and user defined packages are preferred.
""" """
def __init__(self): def __init__(self, pacman: l.Pacman):
self._pacman = pacman
self._package_info_cache: dict[str, PackageInfo] = {} self._package_info_cache: dict[str, PackageInfo] = {}
self._dep_provider_cache: dict[str, PackageInfo] = {} self._dep_provider_cache: dict[str, PackageInfo] = {}
self._user_packages: list[PackageInfo] = [] self._user_packages: list[PackageInfo] = []
@@ -268,6 +246,12 @@ class ExtendedPackageSearch:
Tried caching the given packages. Virtual packages may not be cached Tried caching the given packages. Virtual packages may not be cached
""" """
packages = list(
filter(lambda p: p not in self._package_info_cache, packages))
if len(packages) == 0:
return
l.print_debug(f"Trying to cache {packages}.") l.print_debug(f"Trying to cache {packages}.")
max_pkgs_per_request = 200 max_pkgs_per_request = 200
@@ -306,13 +290,12 @@ class ExtendedPackageSearch:
pkgbase=result["PackageBase"], pkgbase=result["PackageBase"],
version=result["Version"], version=result["Version"],
dependencies=result.get("Depends", []), dependencies=result.get("Depends", []),
make_and_check_dependencies=result.get( make_dependencies=result.get("MakeDepends", []),
"MakeDepends", []) + check_dependencies=result.get("CheckDepends", []),
result.get("CheckDepends", []),
provides=result.get("Provides", []), provides=result.get("Provides", []),
git_url= git_url=
f"https://aur.archlinux.org/{result['PackageBase']}.git" f"https://aur.archlinux.org/{result['PackageBase']}.git",
) pacman=self._pacman)
self._package_info_cache[pkgname] = info self._package_info_cache[pkgname] = info
l.print_debug("Request completed.") l.print_debug("Request completed.")
@@ -361,11 +344,12 @@ class ExtendedPackageSearch:
pkgbase=result["PackageBase"], pkgbase=result["PackageBase"],
version=result["Version"], version=result["Version"],
dependencies=result.get("Depends", []), dependencies=result.get("Depends", []),
make_and_check_dependencies=result.get("MakeDepends", []) + make_dependencies=result.get("MakeDepends", []),
result.get("CheckDepends", []), check_dependencies=result.get("CheckDepends", []),
provides=result.get("Provides", []), provides=result.get("Provides", []),
git_url=f"https://aur.archlinux.org/{result['PackageBase']}.git" git_url=
) f"https://aur.archlinux.org/{result['PackageBase']}.git",
pacman=self._pacman)
self._package_info_cache[package] = info self._package_info_cache[package] = info
@@ -472,6 +456,56 @@ class ExtendedPackageSearch:
return info return info
class ResolvedDependencies:
"""
Result of dependency resolution.
"""
def __init__(self):
self.pacman_deps: set[str] = set()
self.foreign_pkgs: set[str] = set()
self.foreign_dep_pkgs: set[str] = set()
self.foreign_build_dep_pkgs: set[str] = set()
self.build_order: list[str] = []
self.packages: dict[str, ForeignPackage] = {}
self._pkgbases_to_pkgs: dict[str, set[str]] = {}
self._pkgs_to_pkgbases: dict[str, str] = {}
def add_pkgbase_info(self, pkgname: str, pkgbase: str):
"""
Adds information about a which package belongs in which package base.
"""
pkgs = self._pkgbases_to_pkgs.get(pkgbase, set())
pkgs.add(pkgname)
self._pkgbases_to_pkgs[pkgbase] = pkgs
self._pkgs_to_pkgbases[pkgname] = pkgbase
def get_pkgbase(self, pkgname: str) -> str:
"""
Returns the package base of an package.
"""
return self._pkgs_to_pkgbases[pkgname]
def get_pkgs_with_common_pkgbase(self, pkgname: str) -> set[str]:
"""
Returns all packages that have the same package base as the given package.
"""
pkgbase = self._pkgs_to_pkgbases[pkgname]
return self._pkgbases_to_pkgs[pkgbase]
def all_pkgbases(self) -> list[str]:
"""
Returns all pkgbases.
"""
return list(self._pkgbases_to_pkgs)
def get_some_pkgname(self, pkgbase: str) -> str:
"""
Returns some package name that the given pkgbase has.
"""
return list(self._pkgbases_to_pkgs[pkgbase])[0]
class ForeignPackageManager: class ForeignPackageManager:
""" """
Class for dealing with AUR/user packages. Class for dealing with AUR/user packages.
@@ -519,56 +553,84 @@ class ForeignPackageManager:
Installs the given AUR/user packages and their dependencies (both pacman/AUR). Installs the given AUR/user packages and their dependencies (both pacman/AUR).
""" """
if as_explicit is None: if len(foreign_pkgs) == 0:
as_explicit = foreign_pkgs return
all_foreign_pkgs, pacman_deps = self.resolve_dependencies(foreign_pkgs) resolved_dependencies = self.resolve_dependencies(foreign_pkgs)
l.print_summary( l.print_summary(
f"The following foreign packages will be installed: {' '.join(map(lambda p: p.name, all_foreign_pkgs))}" "The following foreign packages will be installed explicitly:")
l.print_continuation("")
l.print_continuation(
f"\t{' '.join(resolved_dependencies.foreign_pkgs)}")
l.print_continuation("")
if resolved_dependencies.foreign_dep_pkgs:
l.print_summary(
"The following foreign packages will be installed as dependencies:"
) )
l.print_continuation("")
l.print_continuation(
f"\t{' '.join(resolved_dependencies.foreign_dep_pkgs)}")
l.print_continuation("")
if resolved_dependencies.foreign_build_dep_pkgs:
l.print_summary(
"The following foreign packages will be built in order to install other packages. They will not be installed:"
)
l.print_continuation("")
l.print_continuation(
f"\t{' '.join(resolved_dependencies.foreign_build_dep_pkgs)}")
l.print_continuation("")
if not l.prompt_confirm("Proceed?", default=True): if not l.prompt_confirm("Proceed?", default=True):
raise l.UserFacingError("Installing aborted.") raise l.UserFacingError("Installing aborted.")
l.print_summary( l.print_summary(
"Installing AUR/user package dependencies from pacman.") "Installing AUR/user package dependencies from pacman.")
self._pacman.install_dependencies(list(pacman_deps)) self._pacman.install_dependencies(
list(resolved_dependencies.pacman_deps))
to_install = [] try:
with PackageBuilder(self._search, self._store,
resolved_dependencies) as builder:
while resolved_dependencies.build_order:
to_build = resolved_dependencies.build_order.pop(0)
while all_foreign_pkgs: pkgbase = resolved_dependencies.get_pkgbase(to_build)
pkg_to_build = all_foreign_pkgs.pop(0) package_names = resolved_dependencies.get_pkgs_with_common_pkgbase(
to_build)
# resolve_dependencies gets info for every package so this cannot be None packages = [
pkgbase = self._search.get_package_info( resolved_dependencies.packages[pkgname]
pkg_to_build.name for pkgname in package_names
).pkgbase # pyright: ignore[reportOptionalMemberAccess] ]
with_same_pkgbase = []
for other in all_foreign_pkgs: builder.build_packages(pkgbase, packages, force)
other_pkgbase = self._search.get_package_info( except (subprocess.CalledProcessError, OSError) as e:
other.name raise l.UserFacingError("Failed to build packages.") from e
).pkgbase # pyright: ignore[reportOptionalMemberAccess]
if other_pkgbase == pkgbase:
with_same_pkgbase.append(other)
for other in with_same_pkgbase: if as_explicit is None:
all_foreign_pkgs.remove(other) as_explicit = list(resolved_dependencies.foreign_pkgs)
to_install += self._build_pkg(pkgbase, packages_to_install = list(resolved_dependencies.foreign_pkgs)
[pkg_to_build] + with_same_pkgbase, packages_to_install += list(resolved_dependencies.foreign_dep_pkgs)
force)
if to_install or force: package_files_to_install = []
for pkg in packages_to_install:
built_pkg = self._store.get_package(pkg)
assert built_pkg is not None
_, path = built_pkg
package_files_to_install.append(path)
if package_files_to_install or force:
l.print_summary("Installing AUR/user packages.") l.print_summary("Installing AUR/user packages.")
self._pacman.install_files(to_install, as_explicit) self._pacman.install_files(package_files_to_install, as_explicit)
else: else:
l.print_summary("No packages to install.") l.print_summary("No packages to install.")
def resolve_dependencies( def resolve_dependencies(
self, foreign_packages: list[str] self, foreign_packages: list[str]) -> ResolvedDependencies:
) -> tuple[list[ForeignPackage], set[str]]:
""" """
Resolves AUR/user dependencies of AUR/user packages. Resolves AUR/user dependencies of AUR/user packages.
@@ -583,7 +645,9 @@ class ForeignPackageManager:
l.print_summary("Resolving AUR / user package dependencies.") l.print_summary("Resolving AUR / user package dependencies.")
l.print_debug(f"Packages: {foreign_packages}") l.print_debug(f"Packages: {foreign_packages}")
pacman_deps = set() result = ResolvedDependencies()
result.foreign_pkgs = set(foreign_packages)
graph = DepGraph() graph = DepGraph()
for name in foreign_packages: for name in foreign_packages:
@@ -593,6 +657,23 @@ class ForeignPackageManager:
to_process = list(foreign_packages) to_process = list(foreign_packages)
total_processed = 0 total_processed = 0
def process_dep(pkgname: str, depname: str, add_to: set[str]):
dep_info = self._search.find_provider(depname)
if dep_info is None:
raise l.UserFacingError(
f"Failed to find '{depname}' from AUR or user provided packages."
)
add_to.add(dep_info.pkgname)
l.print_debug(
f"Adding dependency {dep_info.pkgname} to package {pkgname}.")
graph.add_requirement(dep_info.pkgname, pkgname)
if dep_info.pkgname not in seen_packages:
to_process.append(dep_info.pkgname)
seen_packages.add(dep_info.pkgname)
while to_process: while to_process:
pkgname = to_process.pop() pkgname = to_process.pop()
@@ -602,36 +683,25 @@ class ForeignPackageManager:
f"Failed to find '{pkgname}' from AUR or user provided packages." f"Failed to find '{pkgname}' from AUR or user provided packages."
) )
pacman_deps.update(info.pacman_dependencies(self._pacman)) result.pacman_deps.update(info.pacman_dependencies)
depnames = info.all_foreign_dependencies_stripped(self._pacman) result.add_pkgbase_info(pkgname, info.pkgbase)
self._search.try_caching_packages(depnames)
for depname in depnames: build_deps = info.foreign_make_dependencies_stripped + info.foreign_check_dependencies_stripped
dep_info = self._search.find_provider(depname)
if dep_info is None: self._search.try_caching_packages(
raise l.UserFacingError( info.foreign_dependencies_stripped + build_deps)
f"Failed to find '{depname}' from AUR or user provided packages."
)
l.print_debug( for depname in info.foreign_dependencies_stripped:
f"Adding dependency {dep_info.pkgname} to package {pkgname}." process_dep(pkgname, depname, result.foreign_dep_pkgs)
)
graph.add_requirement(dep_info.pkgname, pkgname) for depname in build_deps:
if dep_info.pkgname not in seen_packages: process_dep(pkgname, depname, result.foreign_build_dep_pkgs)
to_process.append(dep_info.pkgname)
seen_packages.add(dep_info.pkgname)
total_processed += 1 total_processed += 1
l.print_info(f"{total_processed}/{len(seen_packages)}.") l.print_info(f"{total_processed}/{len(seen_packages)}.")
l.print_summary("Determining build order.") l.print_summary("Determining build order.")
build_order = self._determine_build_order(graph)
return (build_order, pacman_deps)
def _determine_build_order(self, graph: DepGraph) -> list[ForeignPackage]:
build_order = []
while True: while True:
to_add = graph.get_and_remove_outer_dep_pkgs() to_add = graph.get_and_remove_outer_dep_pkgs()
@@ -639,65 +709,104 @@ class ForeignPackageManager:
break break
for pkg in to_add: for pkg in to_add:
if pkg not in build_order: if pkg not in result.packages:
l.print_debug(f"Adding {pkg} to build_order.") l.print_debug(f"Adding {pkg} to build_order.")
build_order.append(pkg) result.build_order.append(pkg.name)
result.packages[pkg.name] = pkg
return build_order return result
def _build_pkg(self, package_base: str, packages: list[ForeignPackage], def should_upgrade_package(self,
force: bool) -> list[str]: package: str,
installed_version: str,
fetched_version: str,
upgrade_devel=False) -> bool:
""" """
Builds package(s) with the same package base. Returns a list of package files to install. Returns True if a package should be upgraded.
""" """
package_names = list(map(lambda p: p.name, packages)) if upgrade_devel and is_devel(package):
return True
# Rebuild is only needed if at least one package is not in the cache.
if self._are_all_pkgs_cached(packages) and not force:
l.print_summary(
f"Skipped building '{' '.join(package_names)}'. Already up to date."
)
return []
l.print_summary(f"To build '{' '.join(package_names)}'.")
chroot_pacman_pkgs, chroot_pkg_files = self._get_chroot_packages(
packages)
chroot_dir = os.path.join(conf.build_dir, "chroot")
pkgbuild_dir = os.path.join(conf.build_dir, "pkgbuild")
l.print_debug(
f"Chroot dir is: '{chroot_dir}', pkgbuild dir is '{pkgbuild_dir}'."
)
prev_wd = os.getcwd()
try: try:
os.makedirs(conf.pkg_cache_dir, exist_ok=True) result = int(
subprocess.run(conf.commands.compare_versions(
installed_version, fetched_version),
check=True,
stdout=subprocess.PIPE).stdout.decode())
return result < 0
except (ValueError, subprocess.CalledProcessError) as error:
raise l.UserFacingError("Failed to compare versions.") from error
class PackageBuilder:
"""
Used for building packages in a chroot.
"""
always_included_packages = ["base-devel", "git"]
def __init__(self, search: ExtendedPackageSearch, store: l.Store,
resolved_deps: ResolvedDependencies):
self._search = search
self._store = store
self._resolved_deps = resolved_deps
self.chroot_wd_dir = os.path.join(conf.build_dir, "chroot")
self.chroot_dir = os.path.join(self.chroot_wd_dir, "root")
self.pkgbase_dir_map = {}
self.original_wd = ""
def __enter__(self):
self.store_wd()
self.create_build_environment()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.restore_wd()
self.remove_build_environment()
def store_wd(self):
"""
Remembers the current working directory as the original working directory.
"""
self.original_wd = os.getcwd()
def restore_wd(self):
"""
Returns to the original working directory.
"""
os.chdir(self.original_wd)
def create_build_environment(self):
"""
Creates a new chroot and clones all PKGBUILDS.
"""
l.print_summary("Creating a build environment..")
if os.path.exists(conf.build_dir): if os.path.exists(conf.build_dir):
l.print_info("Removing previous build directory.") l.print_info("Removing previous build directory.")
shutil.rmtree(conf.build_dir) self.remove_build_environment()
l.print_info("Setting up build directory.") l.print_info("Getting all PKGBUILDS.")
# Set up PKGBUILDS
for pkgbase in self._resolved_deps.all_pkgbases():
pkgbuild_dir = os.path.join(conf.build_dir, pkgbase)
self.pkgbase_dir_map[pkgbase] = pkgbuild_dir
os.makedirs(pkgbuild_dir) os.makedirs(pkgbuild_dir)
os.makedirs(chroot_dir)
os.chdir(pkgbuild_dir) os.chdir(pkgbuild_dir)
git_url = self._search.get_package_info( git_url = self._search.get_package_info(
package_names[0] self._resolved_deps.get_some_pkgname(pkgbase)
).git_url # pyright: ignore[reportOptionalMemberAccess] ).git_url # pyright: ignore[reportOptionalMemberAccess]
l.print_debug(f"Git URL for '{package_base}' is '{git_url}'") l.print_debug(f"Git URL for '{pkgbase}' is '{git_url}'")
self._git_clone_and_review_pkgbuild(pkgbase, git_url)
self.git_clone_and_review_pkgbuild(package_base, git_url)
shutil.chown(pkgbuild_dir, user=conf.makepkg_user) shutil.chown(pkgbuild_dir, user=conf.makepkg_user)
l.print_summary(f"Building: '{' '.join(package_names)}'.") l.print_summary("Creating a new chroot.")
os.makedirs(self.chroot_wd_dir)
# Remove GNUPGHOME from mkarchroot environment variables since it may interfere with # Remove GNUPGHOME from mkarchroot environment variables since it may interfere with
# the chroot creation # the chroot creation
@@ -709,49 +818,87 @@ class ForeignPackageManager:
except KeyError: except KeyError:
pass pass
l.print_info("Creating a new chroot.")
subprocess.run(conf.commands.make_chroot( subprocess.run(conf.commands.make_chroot(
os.path.join(chroot_dir, "root"), self.chroot_dir, PackageBuilder.always_included_packages +
["base-devel"] + chroot_pacman_pkgs), list(self._resolved_deps.pacman_deps)),
env=mkarchroot_env_vars, env=mkarchroot_env_vars,
check=True, check=True,
capture_output=conf.quiet_output) capture_output=conf.quiet_output)
def remove_build_environment(self):
"""
Deletes the build environment.
"""
shutil.rmtree(conf.build_dir)
def build_packages(self, package_base: str, packages: list[ForeignPackage],
force: bool):
"""
Builds package(s) with the same package base.
Set force to true to force rebuilds of packages that are already cached
"""
package_names = list(map(lambda p: p.name, packages))
# Rebuild is only needed if at least one package is not in the cache.
if self._are_all_pkgs_cached(packages) and not force:
l.print_summary(
f"Skipped building '{' '.join(package_names)}'. Already up to date."
)
return
l.print_summary(f"To build '{' '.join(package_names)}'.")
chroot_new_pacman_pkgs, chroot_pkg_files = self._get_chroot_packages(
packages)
pkgbuild_dir = self.pkgbase_dir_map[package_base]
os.chdir(pkgbuild_dir)
l.print_debug(
f"Chroot dir is: '{self.chroot_dir}', pkgbuild dir is '{pkgbuild_dir}'."
)
l.print_info("Installing build dependencies to chroot.")
subprocess.run(conf.commands.install_chroot_packages(
self.chroot_dir,
chroot_new_pacman_pkgs + PackageBuilder.always_included_packages),
check=True,
capture_output=conf.quiet_output)
l.print_info("Making package.") l.print_info("Making package.")
subprocess.run(conf.commands.make_chroot_pkg( subprocess.run(conf.commands.make_chroot_pkg(self.chroot_wd_dir,
chroot_dir, conf.makepkg_user, chroot_pkg_files), conf.makepkg_user,
chroot_pkg_files),
check=True, check=True,
capture_output=conf.quiet_output) capture_output=conf.quiet_output)
package_files = []
for pkgname in package_names: for pkgname in package_names:
file = self._find_pkgfile(pkgname, pkgbuild_dir) file = self._find_pkgfile(pkgname, pkgbuild_dir)
dest = shutil.copy(file, conf.pkg_cache_dir) dest = shutil.copy(file, conf.pkg_cache_dir)
version = self._search.get_package_info( version = self._search.get_package_info(
pkgname pkgname).version # pyright: ignore[reportOptionalMemberAccess]
).version # pyright: ignore[reportOptionalMemberAccess]
l.print_debug( l.print_debug(
f"Adding '{pkgname}', version: '{version}' to cache as file '{dest}'." f"Adding '{pkgname}', version: '{version}' to cache as file '{dest}'."
) )
self._store.add_package_to_cache(pkgname, version, dest) self._store.add_package_to_cache(pkgname, version, dest)
package_files.append(dest)
l.print_info("Removing build dependencies from chroot.")
subprocess.run(conf.commands.remove_chroot_packages(
self.chroot_dir, chroot_new_pacman_pkgs),
check=True,
capture_output=conf.quiet_output)
l.print_summary(f"Finished building: '{' '.join(package_names)}'.") l.print_summary(f"Finished building: '{' '.join(package_names)}'.")
except (subprocess.CalledProcessError, OSError) as error:
raise l.UserFacingError(
f"Failed to build package(s) '{' '.join(map(lambda p: p.name, packages))}'."
) from error
finally:
os.chdir(prev_wd)
return package_files
def _are_all_pkgs_cached(self, pkgs: list[ForeignPackage]) -> bool: def _are_all_pkgs_cached(self, pkgs: list[ForeignPackage]) -> bool:
for pkg in pkgs: for pkg in pkgs:
@@ -764,7 +911,7 @@ class ForeignPackageManager:
pkg.name pkg.name
).version # pyright: ignore[reportOptionalMemberAccess] ).version # pyright: ignore[reportOptionalMemberAccess]
if cached_version != fetched_version or self.is_devel(pkg.name): if cached_version != fetched_version or is_devel(pkg.name):
return False return False
return True return True
@@ -772,18 +919,23 @@ class ForeignPackageManager:
self, pkgs_to_build: list[ForeignPackage] self, pkgs_to_build: list[ForeignPackage]
) -> tuple[list[str], list[str]]: ) -> tuple[list[str], list[str]]:
""" """
Returns a tuple of pacman packages and built foreign pkgs files that are needed in the Returns a tuple of pacman build dependencies and built foreign pkgs files that are needed
chroot before building. pkgs_to_build share the same pkgbase. in the chroot before building. pkgs_to_build share the same pkgbase.
""" """
chroot_pacman_pkgs = set() chroot_pacman_build_deps = set()
chroot_foreign_pkgs = set() chroot_foreign_pkgs = set()
def add_to_pacman_build_deps(deps: list[str]):
for dep in deps:
if dep not in self._resolved_deps.pacman_deps:
chroot_pacman_build_deps.add(dep)
for pkg in pkgs_to_build: for pkg in pkgs_to_build:
info = self._search.get_package_info(pkg.name) info = self._search.get_package_info(pkg.name)
assert info is not None assert info is not None
chroot_pacman_pkgs.update( add_to_pacman_build_deps(info.pacman_make_dependencies)
info.all_pacman_dependencies(self._pacman)) add_to_pacman_build_deps(info.pacman_check_dependencies)
foreign_deps = pkg.get_all_recursive_foreign_deps() foreign_deps = pkg.get_all_recursive_foreign_deps()
chroot_foreign_pkgs.update(foreign_deps) chroot_foreign_pkgs.update(foreign_deps)
@@ -792,8 +944,9 @@ class ForeignPackageManager:
for dep in foreign_deps: for dep in foreign_deps:
dep_info = self._search.get_package_info(dep) dep_info = self._search.get_package_info(dep)
assert dep_info is not None assert dep_info is not None
chroot_pacman_pkgs.update(
dep_info.all_pacman_dependencies(self._pacman)) add_to_pacman_build_deps(dep_info.pacman_make_dependencies)
add_to_pacman_build_deps(dep_info.pacman_check_dependencies)
# Packages with the same pkgbase might depend on each other, # Packages with the same pkgbase might depend on each other,
# but they don't need to be installed for the build to succeed. # but they don't need to be installed for the build to succeed.
@@ -812,7 +965,7 @@ class ForeignPackageManager:
chroot_foreign_pkg_files.append(file) chroot_foreign_pkg_files.append(file)
return (list(chroot_pacman_pkgs), chroot_foreign_pkg_files) return (list(chroot_pacman_build_deps), chroot_foreign_pkg_files)
def _find_pkgfile(self, pkgname: str, pkgbuild_dir: str) -> str: def _find_pkgfile(self, pkgname: str, pkgbuild_dir: str) -> str:
# HACK: Because we don't know the pkgarch we can't be sure what is the build result. # HACK: Because we don't know the pkgarch we can't be sure what is the build result.
@@ -838,27 +991,33 @@ class ForeignPackageManager:
return matches[0] return matches[0]
def git_clone_and_review_pkgbuild(self, pkgbase: str, git_url: str): def _git_clone_and_review_pkgbuild(self, pkgbase: str, git_url: str):
""" """
Clones an PKGBUILD to the current directory. Clones an PKGBUILD to the current directory.
The user is prompted to review the PKGBUILD and confirm if the package should be built. The user is prompted to review the PKGBUILD and confirm if the package should be built.
""" """
try: try:
subprocess.run(conf.commands.git_clone(git_url, "."), check=True) subprocess.run(conf.commands.git_clone(git_url, "."),
check=True,
capture_output=conf.quiet_output)
if l.prompt_confirm(f"Review PKGBUILD for {pkgbase}?",
default=True):
latest_reviewed_commit = self._store.pkgbuild_latest_reviewed_commits.get( latest_reviewed_commit = self._store.pkgbuild_latest_reviewed_commits.get(
pkgbase) pkgbase)
if latest_reviewed_commit is None: if latest_reviewed_commit is None:
for file in os.scandir("."): for file in os.scandir("."):
if file.is_file() and not file.name.startswith("."): if file.is_file() and not file.name.startswith("."):
subprocess.run(conf.commands.review_file(file.path), subprocess.run(conf.commands.review_file(
file.path),
check=True) check=True)
else: else:
subprocess.run(conf.commands.git_diff(latest_reviewed_commit), subprocess.run(
conf.commands.git_diff(latest_reviewed_commit),
check=True) check=True)
if l.prompt_confirm("Proceed with building?", default=True): if l.prompt_confirm("Build this package?", default=True):
commit_id = subprocess.run( commit_id = subprocess.run(
conf.commands.git_get_commit_id(), conf.commands.git_get_commit_id(),
check=True, check=True,
@@ -872,42 +1031,3 @@ class ForeignPackageManager:
raise l.UserFacingError( raise l.UserFacingError(
f"Failed to clone and review PKGBUILD from {git_url}" f"Failed to clone and review PKGBUILD from {git_url}"
) from error ) from error
def should_upgrade_package(self,
package: str,
installed_version: str,
fetched_version: str,
upgrade_devel=False) -> bool:
"""
Returns True if a package should be upgraded.
"""
if upgrade_devel and self.is_devel(package):
return True
try:
result = int(
subprocess.run(conf.commands.compare_versions(
installed_version, fetched_version),
check=True,
stdout=subprocess.PIPE).stdout.decode())
return result < 0
except (ValueError, subprocess.CalledProcessError) as error:
raise l.UserFacingError("Failed to compare versions.") from error
def is_devel(self, package: str) -> bool:
"""
Returns True if the given package is a devel package.
"""
devel_suffixes = [
"-git",
"-hg",
"-bzr",
"-svn",
"-cvs",
"-darcs",
]
for suffix in devel_suffixes:
if package.endswith(suffix):
return True
return False
+9 -3
View File
@@ -8,8 +8,9 @@ from decman.lib.aur import ForeignPackageManager, DepGraph, ForeignPackage, Exte
class TestAUR(unittest.TestCase): class TestAUR(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.aur = ForeignPackageManager(Store(), Pacman(), pacman = Pacman()
ExtendedPackageSearch()) self.aur = ForeignPackageManager(Store(), pacman,
ExtendedPackageSearch(pacman))
def test_should_upgrade_package_returns_true_on_newer_version(self): def test_should_upgrade_package_returns_true_on_newer_version(self):
self.assertTrue( self.assertTrue(
@@ -59,6 +60,8 @@ class TestDepGraph(unittest.TestCase):
graph = DepGraph() graph = DepGraph()
graph.add_requirement("A", None) graph.add_requirement("A", None)
graph.add_requirement("V", None)
graph.add_requirement("B1", "A") graph.add_requirement("B1", "A")
graph.add_requirement("B2", "A") graph.add_requirement("B2", "A")
graph.add_requirement("B3", "A") graph.add_requirement("B3", "A")
@@ -71,6 +74,8 @@ class TestDepGraph(unittest.TestCase):
graph.add_requirement("C2", "D") graph.add_requirement("C2", "D")
v = ForeignPackage("V")
a = ForeignPackage("A") a = ForeignPackage("A")
a.add_foreign_dependency_packages(["B1", "B2", "B3", "C1", "C2", "D"]) a.add_foreign_dependency_packages(["B1", "B2", "B3", "C1", "C2", "D"])
@@ -90,7 +95,8 @@ class TestDepGraph(unittest.TestCase):
d = ForeignPackage("D") d = ForeignPackage("D")
d.add_foreign_dependency_packages(["C2"]) d.add_foreign_dependency_packages(["C2"])
self.assertCountEqual(graph.get_and_remove_outer_dep_pkgs(), [c2, b3]) self.assertCountEqual(graph.get_and_remove_outer_dep_pkgs(),
[c2, b3, v])
self.assertCountEqual(graph.get_and_remove_outer_dep_pkgs(), [d]) self.assertCountEqual(graph.get_and_remove_outer_dep_pkgs(), [d])
self.assertCountEqual(graph.get_and_remove_outer_dep_pkgs(), [c1]) self.assertCountEqual(graph.get_and_remove_outer_dep_pkgs(), [c1])
self.assertCountEqual(graph.get_and_remove_outer_dep_pkgs(), [b1]) self.assertCountEqual(graph.get_and_remove_outer_dep_pkgs(), [b1])