build: finish builder rework (MR 2252)

Rename build.package() to build.packages() and take a list of packages
to build, since every caller was inside a for loop this simplifies
usage and let's us give nicer log output by doing all the builds first,
so log messages don't get lost in the middle.

Behaviour is cleaned up so this shouuuuld work pretty well now. It
properly descends into dependencies and will build dependencies even if
the package given doesn't need building. Technically this was only done
before during install where the dependencies were recursed in
chroot.apk.install().

It probably makes the most sense to have a mode where it doesn't build
dependencies but warns the user about it, at least when invoked via
pmbootstrap build.

Signed-off-by: Caleb Connolly <caleb@postmarketos.org>
This commit is contained in:
Caleb Connolly 2024-06-10 01:04:13 +02:00 committed by Oliver Smith
parent e00d2a8e6d
commit 6857882cf0
No known key found for this signature in database
GPG key ID: 5AE7F5513E0885CB
7 changed files with 203 additions and 117 deletions

View file

@ -6,4 +6,5 @@ from pmb.build.kconfig import menuconfig
from pmb.build.newapkbuild import newapkbuild from pmb.build.newapkbuild import newapkbuild
from pmb.build.other import copy_to_buildpath, is_necessary, \ from pmb.build.other import copy_to_buildpath, is_necessary, \
index_repo index_repo
from pmb.build._package import BootstrapStage, mount_pmaports, package, output_path from pmb.build._package import BootstrapStage, mount_pmaports, packages, \
output_path, BuildQueueItem, get_apkbuild

View file

@ -2,7 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import datetime import datetime
import enum import enum
from typing import Any, Dict, List, Optional, Set, TypedDict from typing import Any, Callable, Dict, List, Optional, Set, TypedDict
from pmb.core.arch import Arch from pmb.core.arch import Arch
from pmb.core.context import Context from pmb.core.context import Context
from pmb.core.pkgrepo import pkgrepo_paths, pkgrepo_relative_path from pmb.core.pkgrepo import pkgrepo_paths, pkgrepo_relative_path
@ -17,6 +17,7 @@ import pmb.config.pmaports
import pmb.helpers.pmaports import pmb.helpers.pmaports
import pmb.helpers.repo import pmb.helpers.repo
import pmb.helpers.mount import pmb.helpers.mount
import pmb.helpers.package
from pmb.meta import Cache from pmb.meta import Cache
import pmb.parse import pmb.parse
import pmb.parse.apkindex import pmb.parse.apkindex
@ -33,28 +34,6 @@ class BootstrapStage(enum.IntEnum):
# We don't need explicit representations of the other numbers. # We don't need explicit representations of the other numbers.
def get_apkbuild(pkgname, arch):
"""Parse the APKBUILD path for pkgname.
When there is none, try to find it in the binary package APKINDEX files or raise an exception.
:param pkgname: package name to be built, as specified in the APKBUILD
:returns: None or parsed APKBUILD
"""
# Get existing binary package indexes
pmb.helpers.repo.update(arch)
# Get pmaport, skip upstream only packages
pmaport, apkbuild = pmb.helpers.pmaports.get_with_path(pkgname, False)
if pmaport:
pmaport = pkgrepo_relative_path(pmaport)[0]
return pmaport, apkbuild
if pmb.parse.apkindex.providers(pkgname, arch, False):
return None, None
raise RuntimeError("Package '" + pkgname + "': Could not find aport, and"
" could not find this package in any APKINDEX!")
def check_build_for_arch(pkgname: str, arch: Arch): def check_build_for_arch(pkgname: str, arch: Arch):
"""Check if pmaport can be built or exists as binary for a specific arch. """Check if pmaport can be built or exists as binary for a specific arch.
@ -258,7 +237,7 @@ def output_path(arch: Arch, pkgname: str, pkgver: str, pkgrel: str) -> Path:
return arch / f"{pkgname}-{pkgver}-r{pkgrel}.apk" return arch / f"{pkgname}-{pkgver}-r{pkgrel}.apk"
def run_abuild(context: Context, apkbuild, channel, arch, strict=False, force=False, cross=None, def run_abuild(context: Context, apkbuild, channel, arch: Arch, strict=False, force=False, cross=None,
suffix: Chroot=Chroot.native(), src=None, bootstrap_stage=BootstrapStage.NONE): suffix: Chroot=Chroot.native(), src=None, bootstrap_stage=BootstrapStage.NONE):
""" """
Set up all environment variables and construct the abuild command (all Set up all environment variables and construct the abuild command (all
@ -367,92 +346,82 @@ def is_cached_or_cache(arch: Arch, pkgname: str) -> bool:
"""Check if a package is in the built packages cache, if not """Check if a package is in the built packages cache, if not
then mark it as built. We must mark as built before building then mark it as built. We must mark as built before building
to break cyclical dependency loops.""" to break cyclical dependency loops."""
global _package_cache
if arch not in _package_cache: if arch not in _package_cache:
_package_cache[str(arch)] = [] _package_cache[str(arch)] = []
ret = pkgname in _package_cache.get(str(arch), []) ret = pkgname in _package_cache[str(arch)]
if not ret: if not ret:
_package_cache[str(arch)].append(pkgname) _package_cache[str(arch)].append(pkgname)
else:
logging.debug(f"{arch}/{pkgname}: already built")
return ret return ret
def get_apkbuild(pkgname):
"""Parse the APKBUILD path for pkgname.
When there is none, try to find it in the binary package APKINDEX files or raise an exception.
:param pkgname: package name to be built, as specified in the APKBUILD
:returns: None or parsed APKBUILD
"""
# Get pmaport, skip upstream only packages
pmaport, apkbuild = pmb.helpers.pmaports.get_with_path(pkgname, False)
if pmaport:
pmaport = pkgrepo_relative_path(pmaport)[0]
return pmaport, apkbuild
return None, None
class BuildQueueItem(TypedDict): class BuildQueueItem(TypedDict):
name: str name: str
arch: Arch # Arch to build for
aports: str aports: str
apkbuild: Dict[str, Any] apkbuild: Dict[str, Any]
output_path: Path
channel: str
depends: List[str] depends: List[str]
cross: str cross: str
chroot: Chroot chroot: Chroot
def package(context: Context, pkgname, arch: Optional[Arch]=None, force=False, strict=False, # arch is set if we should build for a specific arch
skip_init_buildenv=False, src=None, def process_package(context: Context, queue_build: Callable, pkgname: str,
bootstrap_stage=BootstrapStage.NONE): arch: Optional[Arch], fallback_arch: Arch, force: bool) -> List[str]:
""" # Only build when APKBUILD exists
Build a package and its dependencies with Alpine Linux' abuild. base_aports, base_apkbuild = get_apkbuild(pkgname)
if not base_apkbuild:
if pmb.parse.apkindex.providers(pkgname, fallback_arch, False):
return []
raise RuntimeError(f"{pkgname}: Could not find aport, and"
" could not find this package in any APKINDEX!")
:param pkgname: package name to be built, as specified in the APKBUILD if arch is None:
:param arch: architecture we're building for (default: native) arch = pmb.build.autodetect.arch(base_apkbuild)
:param force: always build, even if not necessary
:param strict: avoid building with irrelevant dependencies installed by
letting abuild install and uninstall all dependencies.
:param skip_init_buildenv: can be set to False to avoid initializing the
build environment. Use this when building
something during initialization of the build
environment (e.g. qemu aarch64 bug workaround)
:param src: override source used to build the package with a local folder
:param bootstrap_stage: pass a BOOTSTRAP= env var with the value to abuild
:returns: None if the build was not necessary
output path relative to the packages folder ("armhf/ab-1-r2.apk")
"""
arch = Arch.native() if arch is None else arch
build_queue: List[BuildQueueItem] = []
# Add a package to the build queue, fetch it's dependency, and
# add record build helpers to installed (e.g. sccache)
def queue_build(aports: str, apkbuild: Dict[str, Any], cross: Optional[str] = None) -> List[str]:
depends = get_depends(context, apkbuild)
chroot = pmb.build.autodetect.chroot(apkbuild, arch)
cross = cross or pmb.build.autodetect.crosscompile(apkbuild, arch)
build_queue.append({
"name": apkbuild["pkgname"],
"aports": aports, # the pmaports source repo (e.g. "systemd")
"apkbuild": apkbuild,
"depends": depends,
"chroot": chroot,
"cross": cross
})
return depends
if is_cached_or_cache(arch, pkgname) and not force: if is_cached_or_cache(arch, pkgname) and not force:
logging.verbose(f"Skipping build for {arch}/{pkgname}, already built") logging.verbose(f"Skipping build for {arch}/{pkgname}, already built")
return return []
# Only build when APKBUILD exists
aports, apkbuild = get_apkbuild(pkgname, arch)
if not apkbuild:
return
if not pmb.build.is_necessary(arch, apkbuild) and not force:
return
# Detect the build environment (skip unnecessary builds)
if not check_build_for_arch(pkgname, arch):
return
logging.debug(f"{arch}/{pkgname}: Generating dependency tree") logging.debug(f"{arch}/{pkgname}: Generating dependency tree")
# Add the package to the build queue # Add the package to the build queue
depends = queue_build(aports.name, apkbuild) depends = get_depends(context, base_apkbuild)
will_build_base = False
if (pmb.build.is_necessary(arch, base_apkbuild) or force) and check_build_for_arch(pkgname, arch):
will_build_base = True
parent = pkgname parent = pkgname
while len(depends): while len(depends):
dep = depends.pop(0) dep = depends.pop(0)
if is_cached_or_cache(arch, dep): if is_cached_or_cache(arch, pmb.helpers.package.remove_operators(dep)):
continue continue
cross = None cross = None
aports, apkbuild = get_apkbuild(dep, arch) aports, apkbuild = get_apkbuild(dep)
if not apkbuild: if not apkbuild:
continue continue
@ -472,43 +441,139 @@ def package(context: Context, pkgname, arch: Optional[Arch]=None, force=False, s
f" of '{parent}' is outdated, but" f" of '{parent}' is outdated, but"
f" pmbootstrap won't build any depends" f" pmbootstrap won't build any depends"
f" since it was started with --no-depends.") f" since it was started with --no-depends.")
logging.verbose(f"{arch}/{dep}: build necessary")
deps = queue_build(aports.name, apkbuild, cross) deps = get_depends(context, apkbuild)
if will_build_base:
queue_build(aports, apkbuild, deps, cross)
else:
logging.info(f"@YELLOW@SKIP:@END@ {arch}/{dep}: is a dependency of"
f" {pkgname} which isn't marked for build. Call with"
f" --force or consider building {dep} manually")
logging.verbose(f"{arch}/{dep}: Inserting {len(deps)} dependencies") logging.verbose(f"{arch}/{dep}: Inserting {len(deps)} dependencies")
depends = deps + depends depends = deps + depends
parent = dep parent = dep
# Queue the package itself after it's dependencies
if will_build_base:
queue_build(base_aports, base_apkbuild, depends)
return depends
def packages(context: Context, pkgnames: List[str], arch: Optional[Arch]=None, force=False, strict=False,
src=None, bootstrap_stage=BootstrapStage.NONE, log_callback: Optional[Callable]=None) -> List[str]:
"""
Build a package and its dependencies with Alpine Linux' abuild.
:param pkgname: package name to be built, as specified in the APKBUILD
:param arch: architecture we're building for (default: native)
:param force: always build, even if not necessary
:param strict: avoid building with irrelevant dependencies installed by
letting abuild install and uninstall all dependencies.
:param src: override source used to build the package with a local folder
:param bootstrap_stage: pass a BOOTSTRAP= env var with the value to abuild
:param log_callback: function to call before building each package instead of
logging. It should accept a single BuildQueueItem parameter.
:returns: None if the build was not necessary
output path relative to the packages folder ("armhf/ab-1-r2.apk")
"""
global _package_cache
build_queue: List[BuildQueueItem] = []
built_packages: Set[str] = set()
# Add a package to the build queue, fetch it's dependency, and
# add record build helpers to installed (e.g. sccache)
def queue_build(aports: Path, apkbuild: Dict[str, Any], depends: List[str], cross: Optional[str] = None) -> List[str]:
# Skip if already queued
name = apkbuild["pkgname"]
if any(item["name"] == name for item in build_queue):
return []
pkg_arch = pmb.build.autodetect.arch(apkbuild) if arch is None else arch
chroot = pmb.build.autodetect.chroot(apkbuild, pkg_arch)
cross = cross or pmb.build.autodetect.crosscompile(apkbuild, pkg_arch)
build_queue.append({
"name": name,
"arch": pkg_arch,
"aports": aports.name, # the pmaports source repo (e.g. "systemd")
"apkbuild": apkbuild,
"output_path": output_path(pkg_arch, apkbuild["pkgname"],
apkbuild["pkgver"], apkbuild["pkgrel"]),
"channel": pmb.config.pmaports.read_config(aports)["channel"],
"depends": depends,
"chroot": chroot,
"cross": cross
})
# If we just queued a package that was request to be built explicitly then
# record it, since we return which packages we actually built
if apkbuild["pkgname"] in pkgnames:
built_packages.add(apkbuild["pkgname"])
return depends
if src and len(pkgnames) > 1:
raise RuntimeError("Can't build multiple packages with --src")
logging.debug(f"Preparing to build {len(pkgnames)} package{'s' if len(pkgnames) > 1 else ''}:")
logging.verbose(f"\t{', '.join(pkgnames)}")
# We sorta-kind maybe supported building packages for multiple architectures in
# a single called to packages(). We need to do a check to make sure that the user
# didn't specify a package that doesn't exist, and we can't just check the source repo
# since we might get called with some perhaps bogus packages that do exist in the binary
# repo but not in the source one, but we need to error if we get a package that doesn't
# exist anywhere, as something is clearly wrong for that to happen.
# The problem is the APKINDEX parsing code doesn't have a way to check all architectures
# so we need this hack.
fallback_arch = arch if arch is not None else pmb.build.autodetect.arch(pkgnames[0])
# Get existing binary package indexes
pmb.helpers.repo.update(fallback_arch)
# Process the packages we've been asked to build, queuing up any
# dependencies that need building as well as the package itself
all_dependencies: List[str] = []
for pkgname in pkgnames:
all_dependencies += process_package(context, queue_build, pkgname, arch, fallback_arch, force)
if not len(build_queue): if not len(build_queue):
return return []
channel: str = pmb.config.pmaports.read_config(aports)["channel"] qlen = len(build_queue)
logging.info(f"@BLUE@BUILD:@END@ {qlen} source package{'s' if qlen > 1 else ''}")
logging.info(f"{len(build_queue)} package(s) to build") for item in build_queue:
logging.info(f"@BLUE@BUILD:@END@ * {item['channel']}/{item['name']}")
cross = None cross = None
while len(build_queue): while len(build_queue):
pkg = build_queue.pop() pkg = build_queue.pop(0)
chroot = pkg["chroot"] chroot = pkg["chroot"]
pkg_arch = pkg["arch"]
output = output_path(arch, pkg["name"], pkg["apkbuild"]["pkgver"], pkg["apkbuild"]["pkgrel"]) channel = pkg["channel"]
logging.info(f"*** Build {channel}/{output}") output = pkg["output_path"]
if not log_callback:
logging.info(f"*** Building {channel}/{output} ***")
else:
log_callback(pkg)
# One time chroot initialization # One time chroot initialization
if pmb.build.init(chroot): if pmb.build.init(chroot):
pmb.build.other.configure_abuild(chroot) pmb.build.other.configure_abuild(chroot)
pmb.build.other.configure_ccache(chroot) pmb.build.other.configure_ccache(chroot)
if "rust" in depends or "cargo" in depends: if "rust" in all_dependencies or "cargo" in all_dependencies:
pmb.chroot.apk.install(["sccache"], chroot) pmb.chroot.apk.install(["sccache"], chroot)
if src: if src:
pmb.chroot.apk.install(["rsync"], chroot) pmb.chroot.apk.install(["rsync"], chroot)
# We only need to init cross compiler stuff once # We only need to init cross compiler stuff once
if not cross: if not cross:
cross = pmb.build.autodetect.crosscompile(pkg["apkbuild"], arch) cross = pmb.build.autodetect.crosscompile(pkg["apkbuild"], pkg_arch)
if cross: if cross:
pmb.build.init_compiler(context, depends, cross, arch) pmb.build.init_compiler(context, all_dependencies, cross, pkg_arch)
if cross == "crossdirect": if cross == "crossdirect":
pmb.chroot.mount_native_into_foreign(chroot) pmb.chroot.mount_native_into_foreign(chroot)
@ -517,9 +582,13 @@ def package(context: Context, pkgname, arch: Optional[Arch]=None, force=False, s
# Build and finish up # Build and finish up
try: try:
run_abuild(context, pkg["apkbuild"], channel, arch, strict, force, cross, run_abuild(context, pkg["apkbuild"], channel, pkg_arch, strict, force, cross,
chroot, src, bootstrap_stage) chroot, src, bootstrap_stage)
except RuntimeError: except RuntimeError:
raise BuildFailedError(f"Couldn't build {output}!") raise BuildFailedError(f"Couldn't build {output}!")
finish(pkg["apkbuild"], channel, arch, output, chroot, strict) finish(pkg["apkbuild"], channel, pkg_arch, output, chroot, strict)
return output
# Clear package cache for the next run
_package_cache = {}
return list(built_packages)

View file

@ -3,16 +3,17 @@
from pathlib import Path from pathlib import Path
from pmb.core.arch import Arch from pmb.core.arch import Arch
from pmb.helpers import logging from pmb.helpers import logging
from typing import Dict, Optional from typing import Any, Dict, Optional, Union
import pmb.config import pmb.config
import pmb.chroot.apk import pmb.chroot.apk
import pmb.helpers.pmaports import pmb.helpers.pmaports
from pmb.core import Chroot, ChrootType, get_context from pmb.core import Chroot, ChrootType, get_context
from pmb.meta import Cache
# FIXME (#2324): type hint Arch # FIXME (#2324): type hint Arch
def arch_from_deviceinfo(pkgname, aport: Path) -> Optional[str]: def arch_from_deviceinfo(pkgname, aport: Path) -> Optional[Arch]:
""" """
The device- packages are noarch packages. But it only makes sense to build The device- packages are noarch packages. But it only makes sense to build
them for the device's architecture, which is specified in the deviceinfo them for the device's architecture, which is specified in the deviceinfo
@ -31,21 +32,25 @@ def arch_from_deviceinfo(pkgname, aport: Path) -> Optional[str]:
# Return its arch # Return its arch
device = pkgname.split("-", 1)[1] device = pkgname.split("-", 1)[1]
arch = pmb.parse.deviceinfo(device).arch arch = pmb.parse.deviceinfo(device).arch
logging.verbose(pkgname + ": arch from deviceinfo: " + arch) logging.verbose(f"{pkgname}: arch from deviceinfo: {arch}")
return arch return arch
def arch(pkgname: str): @Cache("package")
def arch(package: Union[str, Dict[str, Any]]):
""" """
Find a good default in case the user did not specify for which architecture Find a good default in case the user did not specify for which architecture
a package should be built. a package should be built.
:param package: The name of the package or parsed APKBUILD
:returns: arch string like "x86_64" or "armhf". Preferred order, depending :returns: arch string like "x86_64" or "armhf". Preferred order, depending
on what is supported by the APKBUILD: on what is supported by the APKBUILD:
* native arch * native arch
* device arch (this will be preferred instead if build_default_device_arch is true) * device arch (this will be preferred instead if build_default_device_arch is true)
* first arch in the APKBUILD * first arch in the APKBUILD
""" """
pkgname = package["pkgname"] if isinstance(package, dict) else package
aport = pmb.helpers.pmaports.find(pkgname) aport = pmb.helpers.pmaports.find(pkgname)
if not aport: if not aport:
raise FileNotFoundError(f"APKBUILD not found for {pkgname}") raise FileNotFoundError(f"APKBUILD not found for {pkgname}")
@ -53,7 +58,7 @@ def arch(pkgname: str):
if ret: if ret:
return ret return ret
apkbuild = pmb.parse.apkbuild(aport) apkbuild = pmb.parse.apkbuild(aport) if isinstance(package, str) else package
arches = apkbuild["arch"] arches = apkbuild["arch"]
deviceinfo = pmb.parse.deviceinfo() deviceinfo = pmb.parse.deviceinfo()
@ -71,9 +76,10 @@ def arch(pkgname: str):
return preferred_arch_2nd return preferred_arch_2nd
try: try:
return apkbuild["arch"][0] arch_str = apkbuild["arch"][0]
return Arch.from_str(arch_str) if arch_str else Arch.native()
except IndexError: except IndexError:
return None return Arch.native()
def chroot(apkbuild: Dict[str, str], arch: Arch) -> Chroot: def chroot(apkbuild: Dict[str, str], arch: Arch) -> Chroot:

View file

@ -10,6 +10,7 @@ import glob
import pmb.config.pmaports import pmb.config.pmaports
import pmb.helpers.repo import pmb.helpers.repo
import pmb.build import pmb.build
from pmb.build import BuildQueueItem
from pmb.core import Config from pmb.core import Config
from pmb.core import get_context from pmb.core import get_context
@ -112,11 +113,14 @@ class RepoBootstrap(commands.Command):
# Initialize without pmOS binary package repo # Initialize without pmOS binary package repo
pmb.chroot.init(chroot, usr_merge, postmarketos_mirror=False) pmb.chroot.init(chroot, usr_merge, postmarketos_mirror=False)
for package in self.get_packages(bootstrap_line):
self.log_progress(f"building {package}")
bootstrap_stage = int(step.split("bootstrap_", 1)[1]) bootstrap_stage = int(step.split("bootstrap_", 1)[1])
pmb.build.package(self.context, package, self.arch, force=True, def log_wrapper(pkg: BuildQueueItem):
strict=True, bootstrap_stage=bootstrap_stage) self.log_progress(f"building {pkg['name']}")
packages = self.get_packages(bootstrap_line)
pmb.build.packages(self.context, packages, self.arch, force=True,
strict=True, bootstrap_stage=bootstrap_stage,
log_callback=log_wrapper)
self.log_progress("bootstrap complete!") self.log_progress("bootstrap complete!")

View file

@ -125,10 +125,11 @@ def build(args: PmbArgs):
context = get_context() context = get_context()
# Build all packages # Build all packages
for package in args.packages: built = pmb.build.packages(context, args.packages, args.arch, force,
arch_package = args.arch or pmb.build.autodetect.arch(package) strict=args.strict, src=src)
if not pmb.build.package(context, package, arch_package, force,
args.strict, src=src): # Notify about packages that weren't built
for package in set(args.packages) - set(built):
logging.info("NOTE: Package '" + package + "' is up to date. Use" logging.info("NOTE: Package '" + package + "' is up to date. Use"
" 'pmbootstrap build " + package + " --force'" " 'pmbootstrap build " + package + " --force'"
" if needed.") " if needed.")

View file

@ -487,7 +487,7 @@ def print_firewall_info(disabled: bool, arch: Arch):
kernel = get_kernel_package(get_context().config) kernel = get_kernel_package(get_context().config)
if kernel: if kernel:
kernel_apkbuild = pmb.build._package.get_apkbuild(kernel[0], arch) _, kernel_apkbuild = pmb.build.get_apkbuild(kernel[0])
if kernel_apkbuild: if kernel_apkbuild:
opts = kernel_apkbuild["options"] opts = kernel_apkbuild["options"]
apkbuild_has_opt = "pmb:kconfigcheck-nftables" in opts apkbuild_has_opt = "pmb:kconfigcheck-nftables" in opts

View file

@ -98,18 +98,23 @@ def sideload(args: PmbArgs, user: str, host: str, port: str, arch: Arch, copy_ke
arch = ssh_find_arch(args, user, host, port) arch = ssh_find_arch(args, user, host, port)
context = get_context() context = get_context()
to_build = []
for pkgname in pkgnames: for pkgname in pkgnames:
data_repo = pmb.parse.apkindex.package(pkgname, arch, True) data_repo = pmb.parse.apkindex.package(pkgname, arch, True)
apk_file = f"{pkgname}-{data_repo['version']}.apk" apk_file = f"{pkgname}-{data_repo['version']}.apk"
host_path = context.config.work / "packages" / channel / arch / apk_file host_path = context.config.work / "packages" / channel / arch / apk_file
if not host_path.is_file(): if not host_path.is_file():
pmb.build.package(context, pkgname, arch, force=True) to_build.append(pkgname)
if not host_path.is_file():
raise RuntimeError(f"The package '{pkgname}' could not be built")
paths.append(host_path) paths.append(host_path)
if to_build:
pmb.build.packages(context, to_build, arch, force=True)
# Check all the packages actually got builts
for path in paths:
if not path.is_file():
raise RuntimeError(f"The package '{pkgname}' could not be built")
if copy_key: if copy_key:
scp_abuild_key(args, user, host, port) scp_abuild_key(args, user, host, port)