pyodide/pyodide-build/pyodide_build/buildpkg.py

922 lines
28 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Builds a Pyodide package.
"""
import argparse
import cgi
import fnmatch
import hashlib
import json
import os
import shutil
import subprocess
import sys
import sysconfig
import textwrap
from contextlib import contextmanager
from datetime import datetime
from pathlib import Path
from textwrap import dedent
from types import TracebackType
from typing import Any, Generator, NoReturn, TextIO
from urllib import request
from . import pywasmcross
from .common import find_matching_wheels
@contextmanager
def chdir(new_dir: Path) -> Generator[None, None, None]:
orig_dir = Path.cwd()
try:
os.chdir(new_dir)
yield
finally:
os.chdir(orig_dir)
from . import common
from .io import parse_package_config
def _make_whlfile(*args, owner=None, group=None, **kwargs):
return shutil._make_zipfile(*args, **kwargs) # type: ignore[attr-defined]
shutil.register_archive_format("whl", _make_whlfile, description="Wheel file")
shutil.register_unpack_format(
"whl", [".whl", ".wheel"], shutil._unpack_zipfile, description="Wheel file" # type: ignore[attr-defined]
)
def exit_with_stdio(result: subprocess.CompletedProcess[str]) -> NoReturn:
if result.stdout:
print(" stdout:")
print(textwrap.indent(result.stdout, " "))
if result.stderr:
print(" stderr:")
print(textwrap.indent(result.stderr, " "))
raise SystemExit(result.returncode)
class BashRunnerWithSharedEnvironment:
"""Run multiple bash scripts with persistent environment.
Environment is stored to "env" member between runs. This can be updated
directly to adjust the environment, or read to get variables.
"""
def __init__(self, env=None):
if env is None:
env = dict(os.environ)
self._reader: TextIO | None
self._fd_write: int | None
self.env: dict[str, str] = env
def __enter__(self) -> "BashRunnerWithSharedEnvironment":
fd_read, self._fd_write = os.pipe()
self._reader = os.fdopen(fd_read, "r")
return self
def run(self, cmd, **opts):
"""Run a bash script. Any keyword arguments are passed on to subprocess.run."""
assert self._fd_write is not None
assert self._reader is not None
write_env_pycode = ";".join(
[
"import os",
"import json",
f'os.write({self._fd_write}, json.dumps(dict(os.environ)).encode() + b"\\n")',
]
)
write_env_shell_cmd = f"{sys.executable} -c '{write_env_pycode}'"
full_cmd = f"{cmd}\n{write_env_shell_cmd}"
result = subprocess.run(
["bash", "-ce", full_cmd], pass_fds=[self._fd_write], env=self.env, **opts
)
if result.returncode != 0:
print("ERROR: bash command failed")
print(textwrap.indent(cmd, " "))
exit_with_stdio(result)
self.env = json.loads(self._reader.readline())
return result
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
"""Free the file descriptors."""
if self._fd_write:
os.close(self._fd_write)
self._fd_write = None
if self._reader:
self._reader.close()
self._reader = None
@contextmanager
def get_bash_runner():
PYODIDE_ROOT = os.environ["PYODIDE_ROOT"]
env = {
key: os.environ[key]
for key in [
# TODO: Stabilize and document more of these in meta-yaml.md
"PATH",
"PYTHONPATH",
"PYODIDE_ROOT",
"PYTHONINCLUDE",
"NUMPY_LIB",
"PYODIDE_PACKAGE_ABI",
"HOME",
"HOSTINSTALLDIR",
"TARGETINSTALLDIR",
"SYSCONFIG_NAME",
"HOSTSITEPACKAGES",
"PYMAJOR",
"PYMINOR",
"PYMICRO",
"CPYTHONBUILD",
"SIDE_MODULE_CFLAGS",
"SIDE_MODULE_LDFLAGS",
"STDLIB_MODULE_CFLAGS",
"UNISOLATED_PACKAGES",
"WASM_LIBRARY_DIR",
"WASM_PKG_CONFIG_PATH",
"CARGO_BUILD_TARGET",
"CARGO_HOME",
"CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_LINKER",
"RUSTFLAGS",
"PYO3_CONFIG_FILE",
]
} | {"PYODIDE": "1"}
if "PYODIDE_JOBS" in os.environ:
env["PYODIDE_JOBS"] = os.environ["PYODIDE_JOBS"]
env["PKG_CONFIG_PATH"] = env["WASM_PKG_CONFIG_PATH"]
if "PKG_CONFIG_PATH" in os.environ:
env["PKG_CONFIG_PATH"] += f":{os.environ['PKG_CONFIG_PATH']}"
with BashRunnerWithSharedEnvironment(env=env) as b:
b.run(
f"source {PYODIDE_ROOT}/emsdk/emsdk/emsdk_env.sh", stderr=subprocess.DEVNULL
)
yield b
def check_checksum(archive: Path, source_metadata: dict[str, Any]) -> None:
"""
Checks that an archive matches the checksum in the package metadata.
Parameters
----------
archive
the path to the archive we wish to checksum
source_metadata
The source section from meta.yaml.
"""
checksum_keys = {"md5", "sha256"}.intersection(source_metadata)
if not checksum_keys:
return
elif len(checksum_keys) != 1:
raise ValueError(
"Only one checksum should be included in a package "
"setup; found {}.".format(checksum_keys)
)
checksum_algorithm = checksum_keys.pop()
checksum = source_metadata[checksum_algorithm]
CHUNK_SIZE = 1 << 16
h = getattr(hashlib, checksum_algorithm)()
with open(archive, "rb") as fd:
while True:
chunk = fd.read(CHUNK_SIZE)
h.update(chunk)
if len(chunk) < CHUNK_SIZE:
break
if h.hexdigest() != checksum:
raise ValueError(f"Invalid {checksum_algorithm} checksum")
def trim_archive_extension(tarballname):
for extension in [
".tar.gz",
".tgz",
".tar",
".tar.bz2",
".tbz2",
".tar.xz",
".txz",
".zip",
".whl",
]:
if tarballname.endswith(extension):
return tarballname[: -len(extension)]
return tarballname
def download_and_extract(
buildpath: Path, srcpath: Path, src_metadata: dict[str, Any]
) -> None:
"""
Download the source from specified in the meta data, then checksum it, then
extract the archive into srcpath.
Parameters
----------
buildpath
The path to the build directory. Generally will be
$(PYOIDE_ROOT)/packages/<package-name>/build/.
srcpath
The place we want the source to end up. Will generally be
$(PYOIDE_ROOT)/packages/<package-name>/build/<package-name>-<package-version>.
src_metadata
The source section from meta.yaml.
"""
response = request.urlopen(src_metadata["url"])
_, parameters = cgi.parse_header(response.headers.get("Content-Disposition", ""))
if "filename" in parameters:
tarballname = parameters["filename"]
else:
tarballname = Path(response.geturl()).name
tarballpath = buildpath / tarballname
if not tarballpath.is_file():
os.makedirs(tarballpath.parent, exist_ok=True)
with open(tarballpath, "wb") as f:
f.write(response.read())
try:
check_checksum(tarballpath, src_metadata)
except Exception:
tarballpath.unlink()
raise
if tarballpath.suffix == ".whl":
os.makedirs(srcpath / "dist")
shutil.copy(tarballpath, srcpath / "dist")
return
if not srcpath.is_dir():
shutil.unpack_archive(tarballpath, buildpath)
extract_dir_name = src_metadata.get("extract_dir")
if not extract_dir_name:
extract_dir_name = trim_archive_extension(tarballname)
shutil.move(buildpath / extract_dir_name, srcpath)
def prepare_source(
pkg_root: Path, buildpath: Path, srcpath: Path, src_metadata: dict[str, Any]
) -> None:
"""
Figure out from the "source" key in the package metadata where to get the source
from, then get the source into srcpath (or somewhere else, if it goes somewhere
else, returns where it ended up).
Parameters
----------
pkg_root
The path to the root directory for the package. Generally
$PYODIDE_ROOT/packages/<PACKAGES>
buildpath
The path to the build directory. Generally will be
$(PYOIDE_ROOT)/packages/<PACKAGE>/build/.
srcpath
The default place we want the source to end up. Will generally be
$(PYOIDE_ROOT)/packages/<package-name>/build/<package-name>-<package-version>.
src_metadata
The source section from meta.yaml.
Returns
-------
The location where the source ended up. TODO: None, actually?
"""
if buildpath.resolve().is_dir():
shutil.rmtree(buildpath)
os.makedirs(buildpath)
if "url" in src_metadata:
download_and_extract(buildpath, srcpath, src_metadata)
return
if "path" not in src_metadata:
raise ValueError(
"Incorrect source provided. Either a url or a path must be provided."
)
srcdir = Path(src_metadata["path"]).resolve()
if not srcdir.is_dir():
raise ValueError(f"path={srcdir} must point to a directory that exists")
shutil.copytree(srcdir, srcpath)
def patch(pkg_root: Path, srcpath: Path, src_metadata: dict[str, Any]) -> None:
"""
Apply patches to the source.
Parameters
----------
pkg_root
The path to the root directory for the package. Generally
$PYODIDE_ROOT/packages/<PACKAGES>
srcpath
The path to the source. We extract the source into the build directory, so it
will be something like
$(PYOIDE_ROOT)/packages/<PACKAGE>/build/<PACKAGE>-<VERSION>.
src_metadata
The "source" key from meta.yaml.
"""
if (srcpath / ".patched").is_file():
return
patches = src_metadata.get("patches", [])
extras = src_metadata.get("extras", [])
if not patches and not extras:
return
# We checked these in check_package_config.
assert "url" in src_metadata
assert not src_metadata["url"].endswith(".whl")
# Apply all the patches
with chdir(srcpath):
for patch in patches:
result = subprocess.run(
["patch", "-p1", "--binary", "--verbose", "-i", pkg_root / patch],
check=False,
encoding="utf-8",
)
if result.returncode != 0:
print(f"ERROR: Patch {pkg_root/patch} failed")
exit_with_stdio(result)
# Add any extra files
for src, dst in extras:
shutil.copyfile(pkg_root / src, srcpath / dst)
with open(srcpath / ".patched", "wb") as fd:
fd.write(b"\n")
def unpack_wheel(path):
with chdir(path.parent):
result = subprocess.run(
[sys.executable, "-m", "wheel", "unpack", path.name],
check=False,
encoding="utf-8",
)
if result.returncode != 0:
print(f"ERROR: Unpacking wheel {path.name} failed")
exit_with_stdio(result)
def pack_wheel(path):
with chdir(path.parent):
result = subprocess.run(
[sys.executable, "-m", "wheel", "pack", path.name],
check=False,
encoding="utf-8",
)
if result.returncode != 0:
print(f"ERROR: Packing wheel {path} failed")
exit_with_stdio(result)
def compile(
name: str,
srcpath: Path,
build_metadata: dict[str, Any],
bash_runner: BashRunnerWithSharedEnvironment,
*,
target_install_dir: str,
) -> None:
"""
Runs pywasmcross for the package. The effect of this is to first run setup.py
with compiler wrappers subbed in, which don't actually build the package but
write the compile commands to build.log. Then we walk over build log and invoke
the same set of commands but with some flags munged around or removed to make it
work with emcc.
In any case, only works for Python packages, not libraries or shared libraries
which don't have a setup.py.
Parameters
----------
srcpath
The path to the source. We extract the source into the build directory, so it
will be something like
$(PYOIDE_ROOT)/packages/<PACKAGE>/build/<PACKAGE>-<VERSION>.
build_metadata
The build section from meta.yaml.
bash_runner
The runner we will use to execute our bash commands. Preserves environment
variables from one invocation to the next.
target_install_dir
The path to the target Python installation
"""
# This function runs setup.py. library and sharedlibrary don't have setup.py
if build_metadata.get("sharedlibrary"):
return
replace_libs = ";".join(build_metadata.get("replace-libs", []))
with chdir(srcpath):
pywasmcross.compile(
env=bash_runner.env,
pkgname=name,
backend_flags=build_metadata["backend-flags"],
cflags=build_metadata["cflags"],
cxxflags=build_metadata["cxxflags"],
ldflags=build_metadata["ldflags"],
target_install_dir=target_install_dir,
replace_libs=replace_libs,
)
def replace_so_abi_tags(wheel_dir: Path) -> None:
"""Replace native abi tag with emscripten abi tag in .so file names"""
build_soabi = sysconfig.get_config_var("SOABI")
assert build_soabi
ext_suffix = sysconfig.get_config_var("EXT_SUFFIX")
assert ext_suffix
build_triplet = "-".join(build_soabi.split("-")[2:])
host_triplet = common.get_make_flag("PLATFORM_TRIPLET")
for file in wheel_dir.glob(f"**/*{ext_suffix}"):
file.rename(file.with_name(file.name.replace(build_triplet, host_triplet)))
def package_wheel(
pkg_name: str,
pkg_root: Path,
srcpath: Path,
build_metadata: dict[str, Any],
bash_runner: BashRunnerWithSharedEnvironment,
host_install_dir: str,
) -> None:
"""Package a wheel
This unpacks the wheel, unvendors tests if necessary, runs and "build.post"
script, and then repacks the wheel.
Parameters
----------
pkg_name
The name of the package
pkg_root
The path to the root directory for the package. Generally
$PYODIDE_ROOT/packages/<PACKAGES>
srcpath
The path to the source. We extract the source into the build directory,
so it will be something like
$(PYOIDE_ROOT)/packages/<PACKAGE>/build/<PACKAGE>-<VERSION>.
build_metadata
The build section from meta.yaml.
bash_runner
The runner we will use to execute our bash commands. Preserves
environment variables from one invocation to the next.
"""
if build_metadata.get("sharedlibrary"):
return
distdir = srcpath / "dist"
wheel, *rest = find_matching_wheels(distdir.glob("*.whl"))
if rest:
raise Exception(
f"Unexpected number of wheels {len(rest) + 1} when building {pkg_name}"
)
print(f"Unpacking wheel to {str(wheel)}")
unpack_wheel(wheel)
wheel.unlink()
name, ver, _ = wheel.name.split("-", 2)
wheel_dir_name = f"{name}-{ver}"
wheel_dir = distdir / wheel_dir_name
# update so abi tags after build is complete but before running post script
# to maximize sanity.
replace_so_abi_tags(wheel_dir)
post = build_metadata.get("post")
if post:
print("Running post script in ", str(Path.cwd().absolute()))
bash_runner.env.update({"PKGDIR": str(pkg_root), "WHEELDIR": str(wheel_dir)})
result = bash_runner.run(post)
if result.returncode != 0:
print("ERROR: post failed")
exit_with_stdio(result)
python_dir = f"python{sys.version_info.major}.{sys.version_info.minor}"
host_site_packages = Path(host_install_dir) / f"lib/{python_dir}/site-packages"
if build_metadata.get("cross-build-env"):
subprocess.check_call(
["pip", "install", "-t", str(host_site_packages), f"{name}=={ver}"]
)
cross_build_files: list[str] | None = build_metadata.get("cross-build-files")
if cross_build_files:
for file in cross_build_files:
shutil.copy((wheel_dir / file), host_site_packages / file)
test_dir = distdir / "tests"
nmoved = 0
if build_metadata.get("unvendor-tests", True):
nmoved = unvendor_tests(wheel_dir, test_dir)
if nmoved:
with chdir(distdir):
shutil.make_archive(f"{pkg_name}-tests", "tar", test_dir)
pack_wheel(wheel_dir)
# wheel_dir causes pytest collection failures for in-tree packages like
# micropip. To prevent these, we get rid of wheel_dir after repacking the
# wheel.
shutil.rmtree(wheel_dir)
shutil.rmtree(test_dir, ignore_errors=True)
def unvendor_tests(install_prefix: Path, test_install_prefix: Path) -> int:
"""Unvendor test files and folders
This function recursively walks through install_prefix and moves anything
that looks like a test folder under test_install_prefix.
Parameters
----------
install_prefix
the folder where the package was installed
test_install_prefix
the folder where to move the tests. If it doesn't exist, it will be
created.
Returns
-------
n_moved
number of files or folders moved
"""
n_moved = 0
out_files = []
shutil.rmtree(test_install_prefix, ignore_errors=True)
for root, _dirs, files in os.walk(install_prefix):
root_rel = Path(root).relative_to(install_prefix)
if root_rel.name == "__pycache__" or root_rel.name.endswith(".egg_info"):
continue
if root_rel.name in ["test", "tests"]:
# This is a test folder
(test_install_prefix / root_rel).parent.mkdir(exist_ok=True, parents=True)
shutil.move(install_prefix / root_rel, test_install_prefix / root_rel)
n_moved += 1
continue
out_files.append(root)
for fpath in files:
if (
fnmatch.fnmatchcase(fpath, "test_*.py")
or fnmatch.fnmatchcase(fpath, "*_test.py")
or fpath == "conftest.py"
):
(test_install_prefix / root_rel).mkdir(exist_ok=True, parents=True)
shutil.move(
install_prefix / root_rel / fpath,
test_install_prefix / root_rel / fpath,
)
n_moved += 1
return n_moved
def create_packaged_token(buildpath: Path) -> None:
(buildpath / ".packaged").write_text("\n")
def run_script(
buildpath: Path,
srcpath: Path,
build_metadata: dict[str, Any],
bash_runner: BashRunnerWithSharedEnvironment,
) -> None:
"""
Run the build script indicated in meta.yaml
Parameters
----------
buildpath
the package build path. Usually `packages/<name>/build`
srcpath
the package source path. Usually
`packages/<name>/build/<name>-<version>`.
build_metadata
The build section from meta.yaml.
bash_runner
The runner we will use to execute our bash commands. Preserves environment
variables from one invocation to the next.
"""
script = build_metadata.get("script")
if not script:
return
with chdir(srcpath):
result = bash_runner.run(script)
if result.returncode != 0:
print("ERROR: script failed")
exit_with_stdio(result)
def needs_rebuild(
pkg_root: Path, buildpath: Path, source_metadata: dict[str, Any]
) -> bool:
"""
Determines if a package needs a rebuild because its meta.yaml, patches, or
sources are newer than the `.packaged` thunk.
pkg_root
The path to the root directory for the package. Generally
$PYODIDE_ROOT/packages/<PACKAGES>
buildpath
The path to the build directory. Generally will be
$(PYOIDE_ROOT)/packages/<PACKAGE>/build/.
src_metadata
The source section from meta.yaml.
"""
packaged_token = buildpath / ".packaged"
if not packaged_token.is_file():
return True
package_time = packaged_token.stat().st_mtime
def source_files():
yield pkg_root / "meta.yaml"
yield from (
pkg_root / patch_path for patch_path in source_metadata.get("patches", [])
)
yield from (
pkg_root / patch_path
for [patch_path, _] in source_metadata.get("extras", [])
)
src_path = source_metadata.get("path")
if src_path:
yield from (pkg_root / src_path).resolve().glob("**/*")
for source_file in source_files():
source_file = Path(source_file)
if source_file.stat().st_mtime > package_time:
return True
return False
def build_package(
pkg_root: Path,
pkg: dict[str, Any],
*,
target_install_dir: str,
host_install_dir: str,
force_rebuild: bool,
continue_: bool,
) -> None:
"""
Build the package. The main entrypoint in this module.
pkg_root
The path to the root directory for the package. Generally
$PYODIDE_ROOT/packages/<PACKAGES>
pkg
The package metadata parsed from the meta.yaml file in pkg_root
target_install_dir
The path to the target Python installation
host_install_dir
Directory for installing built host packages.
"""
pkg_metadata = pkg["package"]
source_metadata = pkg["source"]
build_metadata = pkg["build"]
name = pkg_metadata["name"]
version = pkg_metadata["version"]
build_dir = pkg_root / "build"
src_dir_name: str = f"{name}-{version}"
srcpath = build_dir / src_dir_name
url = source_metadata.get("url")
finished_wheel = url and url.endswith(".whl")
script = build_metadata.get("script")
library = build_metadata.get("library", False)
sharedlibrary = build_metadata.get("sharedlibrary", False)
post = build_metadata.get("post")
# These are validated in io.check_package_config
# If any of these assertions fail, the code path through here might get a
# bit weird
assert not (library and sharedlibrary)
if finished_wheel:
assert not script
assert not library
assert not sharedlibrary
if post:
assert not library
assert not sharedlibrary
if not force_rebuild and not needs_rebuild(pkg_root, build_dir, source_metadata):
return
if continue_ and not srcpath.exists():
raise OSError(
"Cannot find source for rebuild. Expected to find the source "
f"directory at the path {srcpath}, but that path does not exist."
)
import os
import subprocess
import sys
tee = subprocess.Popen(["tee", pkg_root / "build.log"], stdin=subprocess.PIPE)
# Cause tee's stdin to get a copy of our stdin/stdout (as well as that
# of any child processes we spawn)
os.dup2(tee.stdin.fileno(), sys.stdout.fileno()) # type: ignore[union-attr]
os.dup2(tee.stdin.fileno(), sys.stderr.fileno()) # type: ignore[union-attr]
with chdir(pkg_root), get_bash_runner() as bash_runner:
bash_runner.env["PKG_VERSION"] = version
bash_runner.env["PKG_BUILD_DIR"] = str(srcpath)
if not continue_:
prepare_source(pkg_root, build_dir, srcpath, source_metadata)
patch(pkg_root, srcpath, source_metadata)
run_script(build_dir, srcpath, build_metadata, bash_runner)
if library:
create_packaged_token(build_dir)
return
if not sharedlibrary and not finished_wheel:
compile(
name,
srcpath,
build_metadata,
bash_runner,
target_install_dir=target_install_dir,
)
if not sharedlibrary:
package_wheel(
name, pkg_root, srcpath, build_metadata, bash_runner, host_install_dir
)
shutil.rmtree(pkg_root / "dist", ignore_errors=True)
shutil.copytree(srcpath / "dist", pkg_root / "dist")
if sharedlibrary:
shutil.make_archive(f"{name}-{version}", "zip", pkg_root / "dist")
create_packaged_token(build_dir)
def make_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
parser.description = (
"Build a pyodide package.\n\n"
"Note: this is a private endpoint that should not be used "
"outside of the Pyodide Makefile."
)
parser.add_argument(
"package", type=str, nargs=1, help="Path to meta.yaml package description"
)
parser.add_argument(
"--cflags",
type=str,
nargs="?",
default=common.get_make_flag("SIDE_MODULE_CFLAGS"),
help="Extra compiling flags",
)
parser.add_argument(
"--cxxflags",
type=str,
nargs="?",
default=common.get_make_flag("SIDE_MODULE_CXXFLAGS"),
help="Extra C++ specific compiling flags",
)
parser.add_argument(
"--ldflags",
type=str,
nargs="?",
default=common.get_make_flag("SIDE_MODULE_LDFLAGS"),
help="Extra linking flags",
)
parser.add_argument(
"--target-install-dir",
type=str,
nargs="?",
default=common.get_make_flag("TARGETINSTALLDIR"),
help="The path to the target Python installation",
)
parser.add_argument(
"--host-install-dir",
type=str,
nargs="?",
default=common.get_make_flag("HOSTINSTALLDIR"),
help=(
"Directory for installing built host packages. Defaults to setup.py "
"default. Set to 'skip' to skip installation. Installation is "
"needed if you want to build other packages that depend on this one."
),
)
parser.add_argument(
"--force-rebuild",
action="store_true",
help=(
"Force rebuild of package regardless of whether it appears to have been updated"
),
)
parser.add_argument(
"--continue",
dest="continue_",
action="store_true",
help=(
dedent(
"""
Continue a build from the middle. For debugging. Implies "--force-rebuild".
"""
).strip()
),
)
return parser
def main(args):
continue_ = not not args.continue_
# --continue implies --force-rebuild
force_rebuild = args.force_rebuild or continue_
meta_file = Path(args.package[0]).resolve()
pkg_root = meta_file.parent
pkg = parse_package_config(meta_file)
pkg["source"] = pkg.get("source", {})
pkg["build"] = pkg.get("build", {})
build_metadata = pkg["build"]
build_metadata["backend-flags"] = build_metadata.get("backend-flags", "")
build_metadata["cflags"] = build_metadata.get("cflags", "")
build_metadata["cxxflags"] = build_metadata.get("cxxflags", "")
build_metadata["ldflags"] = build_metadata.get("ldflags", "")
build_metadata["cflags"] += f" {args.cflags}"
build_metadata["cxxflags"] += f" {args.cxxflags}"
build_metadata["ldflags"] += f" {args.ldflags}"
name = pkg["package"]["name"]
t0 = datetime.now()
print("[{}] Building package {}...".format(t0.strftime("%Y-%m-%d %H:%M:%S"), name))
success = True
try:
build_package(
pkg_root,
pkg,
target_install_dir=args.target_install_dir,
host_install_dir=args.host_install_dir,
force_rebuild=force_rebuild,
continue_=continue_,
)
except Exception:
success = False
raise
finally:
t1 = datetime.now()
datestamp = "[{}]".format(t1.strftime("%Y-%m-%d %H:%M:%S"))
total_seconds = f"{(t1 - t0).total_seconds():.1f}"
status = "Succeeded" if success else "Failed"
print(
f"{datestamp} {status} building package {name} in {total_seconds} seconds."
)
if __name__ == "__main__":
parser = make_parser(argparse.ArgumentParser())
args = parser.parse_args()
main(args)