blob: d056bb015fea6904d2576ec5934f136cbb3bb374 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2018 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Common ssh_client util code."""
import logging
import multiprocessing
import os
from pathlib import Path
import re
import shutil
import sys
BIN_DIR = Path(__file__).resolve().parent
DIR = BIN_DIR.parent
LIBAPPS_DIR = DIR.parent
sys.path.insert(0, str(LIBAPPS_DIR / "libdot" / "bin"))
import libdot # pylint: disable=wrong-import-position
# The top output directory. Everything lives under this.
OUTPUT = DIR / "output"
# Where archives are cached for fetching & unpacking.
DISTDIR = OUTPUT / "distfiles"
# All package builds happen under this path.
BUILDDIR = OUTPUT / "build"
# Directory to put build-time tools.
BUILD_BINDIR = OUTPUT / "bin"
# Some tools like to scribble in $HOME.
HOME = OUTPUT / "home"
# Where we save shared libs and headers.
SYSROOT = OUTPUT / "sysroot"
# Base path to our source mirror.
SRC_URI_MIRROR = (
"https://commondatastorage.googleapis.com/"
"chromeos-localmirror/secureshell"
)
# Number of jobs for parallel operations.
JOBS = multiprocessing.cpu_count()
# Help simplify the API for users of ssh_client.py.
run = libdot.run
symlink = libdot.symlink
touch = libdot.touch
unlink = libdot.unlink
def copy(source, dest):
"""Always copy |source| to |dest|."""
logging.info("Copying %s -> %s", source, dest)
os.makedirs(os.path.dirname(dest), exist_ok=True)
# In case the dest perms are broken, remove the file.
if os.path.exists(dest):
unlink(dest)
shutil.copy2(source, dest)
def emake(*args, **kwargs):
"""Run `make` with |args| and automatic -j."""
jobs = kwargs.pop("jobs", JOBS)
run(["make", f"-j{jobs}"] + list(args), **kwargs)
def fetch(uri=None, name=None):
"""Download |uri| into DISTDIR as |name|."""
if uri is None:
uri = "/".join((SRC_URI_MIRROR, name))
if name is None:
name = os.path.basename(uri)
libdot.fetch(uri, DISTDIR / name)
def stamp_name(workdir, phase, unique):
"""Get a unique name for this particular step.
This is useful for checking whether certain steps have finished (and thus
have been given a completion "stamp").
Args:
workdir: The package-unique work directory.
phase: The phase we're in e.g. "unpack" or "prepare".
unique: A unique name for the step we're checking in this phase.
Returns:
The full file path to the stamp file.
"""
return os.path.join(workdir, f".stamp.{phase}.{unique}")
def unpack(archive, cwd=None, workdir=None):
"""Unpack |archive| into |cwd|."""
distfile = DISTDIR / archive
stamp = stamp_name(workdir, "unpack", distfile.name)
if workdir and os.path.exists(stamp):
logging.info("Archive already unpacked: %s", archive)
else:
libdot.unpack(distfile, cwd=workdir or cwd)
touch(stamp)
def parse_metadata(metadata):
"""Turn the |metadata| file into a dict."""
ret = {}
re_field = re.compile(r'^(name|version): "(.*)"')
with open(metadata, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
m = re_field.match(line)
if m:
ret[m.group(1)] = m.group(2)
return ret
class ToolchainInfo:
"""Information about the active toolchain environment."""
def __init__(self, env):
"""Initialize."""
self._env = env
if not env:
return
self._cbuild = None
self.chost = self._env["CHOST"]
self.sysroot = Path(self._env["SYSROOT"])
self.libdir = self.sysroot / "lib"
self.incdir = self.sysroot / "include"
self.pkgconfdir = self.libdir / "pkgconfig"
self.ar = self._env["AR"]
@classmethod
def from_id(cls, name):
"""Figure out what environment should be used."""
if name == "pnacl":
return cls(_toolchain_pnacl_env())
elif name == "wasm":
return cls(_toolchain_wasm_env())
assert name == "build"
return cls({})
def activate(self):
"""Update the current environment with this toolchain."""
os.environ.update(self._env)
@property
def cbuild(self):
"""Get the current build system."""
if self._cbuild is None:
prog = BUILD_BINDIR / "config.guess"
result = run([prog], capture_output=True)
self._cbuild = result.stdout.strip().decode("utf-8")
return self._cbuild
def _toolchain_pnacl_env():
"""Get custom env to build using PNaCl toolchain."""
nacl_sdk_root = OUTPUT / "naclsdk"
toolchain_root = nacl_sdk_root / "toolchain" / "linux_pnacl"
bin_dir = toolchain_root / "bin"
compiler_prefix = str(bin_dir / "pnacl-")
sysroot = toolchain_root / "le32-nacl"
sysroot_libdir = sysroot / "lib"
pkgconfig_dir = sysroot_libdir / "pkgconfig"
return {
"CHOST": "nacl",
"NACL_ARCH": "pnacl",
"NACL_SDK_ROOT": str(nacl_sdk_root),
"PATH": os.path.sep.join((str(bin_dir), os.environ["PATH"])),
"CC": compiler_prefix + "clang",
"CXX": compiler_prefix + "clang++",
"AR": compiler_prefix + "ar",
"RANLIB": compiler_prefix + "ranlib",
"STRIP": compiler_prefix + "strip",
"PKG_CONFIG_PATH": str(pkgconfig_dir),
"PKG_CONFIG_LIBDIR": str(sysroot_libdir),
"SYSROOT": str(sysroot),
"CPPFLAGS": (
f"-I{sysroot / 'include' / 'glibc-compat'}"
f" -I{nacl_sdk_root / 'include'}"
),
"LDFLAGS": f"-L{nacl_sdk_root / 'lib' / 'pnacl' / 'Release'}",
}
def _toolchain_wasm_env():
"""Get custom env to build using WASM toolchain."""
sdk_root = OUTPUT / "wasi-sdk"
bin_dir = sdk_root / "bin"
sysroot = sdk_root / "share" / "wasi-sysroot"
libdir = sysroot / "lib"
incdir = sysroot / "include"
pcdir = libdir / "pkgconfig"
return {
# Only use single core here due to known bug in 89 release:
# https://github.com/WebAssembly/binaryen/issues/2273
"BINARYEN_CORES": "1",
"ac_cv_func_calloc_0_nonnull": "yes",
"ac_cv_func_malloc_0_nonnull": "yes",
"ac_cv_func_realloc_0_nonnull": "yes",
"CHOST": "wasm32-wasi",
"CC": f"{bin_dir / 'clang'} --sysroot={sysroot}",
"CXX": f"{bin_dir / 'clang++'} --sysroot={sysroot}",
"AR": str(bin_dir / "llvm-ar"),
"RANLIB": str(bin_dir / "llvm-ranlib"),
"STRIP": str(BUILD_BINDIR / "wasm-strip"),
"PKG_CONFIG_SYSROOT_DIR": str(sysroot),
"PKG_CONFIG_LIBDIR": str(pcdir),
"SYSROOT": str(sysroot),
"CPPFLAGS": " ".join((f'-isystem {incdir / "wassh-libc-sup"}',)),
"LDFLAGS": " ".join(
[
f"-L{libdir}",
"-lwassh-libc-sup",
"-lwasi-emulated-signal",
(
"-Wl,--allow-undefined-file="
f"{libdir / 'wassh-libc-sup.imports'}"
),
"-Wl,--export=__wassh_signal_deliver",
]
),
}
def default_src_unpack(metadata):
"""Default src_unpack phase."""
for archive in metadata["archives"]:
name = archive % metadata
fetch(name=name)
unpack(name, workdir=metadata["workdir"])
def default_src_prepare(metadata):
"""Default src_prepare phase."""
filesdir = metadata["filesdir"]
workdir = metadata["workdir"]
for patch in metadata["patches"]:
patch = patch % metadata
name = os.path.basename(patch)
stamp = stamp_name(workdir, "prepare", name)
if os.path.exists(stamp):
logging.info("Patch already applied: %s", name)
else:
patch = filesdir / patch
logging.info("Applying patch %s", name)
with patch.open("rb") as fp:
run(["patch", "-p1"], stdin=fp)
touch(stamp)
def default_src_configure(_metadata):
"""Default src_configure phase."""
def default_src_compile(_metadata):
"""Default src_compile phase."""
if os.path.exists("Makefile"):
emake()
def default_src_install(_metadata):
"""Default src_install phase."""
def get_parser(desc, default_toolchain):
"""Get a command line parser."""
parser = libdot.ArgumentParser(description=desc)
parser.add_argument(
"--toolchain",
choices=("build", "pnacl", "wasm"),
default=default_toolchain,
help="Which toolchain to use (default: %(default)s).",
)
parser.add_argument(
"-j", "--jobs", type=int, help="Number of jobs to use in parallel."
)
return parser
def update_gnuconfig(metadata, sourcedir):
"""Update config.guess/config.sub files in |sourcedir|."""
# Special case the sorce of gnuconfig.
if metadata["PN"] == "gnuconfig":
return
for prog in ("config.guess", "config.sub"):
source = BUILD_BINDIR / prog
target = os.path.join(sourcedir, prog)
if os.path.exists(source) and os.path.exists(target):
copy(source, target)
def build_package(module, default_toolchain):
"""Build the package in the |module|.
The file system layout is:
output/ OUTPUT
build/ BUILDDIR
build/ toolchain
mandoc-1.14.3/ metadata['basedir']
work/ metadata['workdir']
$p/ metadata['S']
temp/ metadata['T']
pnacl/ toolchain
zlib-1.2.11/ metadata['basedir']
work/ metadata['workdir']
$p/ metadata['S']
temp/ metadata['T']
"""
parser = get_parser(module.__doc__, default_toolchain)
opts = parser.parse_args()
if opts.jobs:
global JOBS # pylint: disable=global-statement
JOBS = opts.jobs
# Create a metadata object from the METADATA file and other settings.
# This object will be used throughout the build to pass around vars.
filesdir = getattr(module, "FILESDIR")
metadata_file = os.path.join(filesdir, "METADATA")
metadata = parse_metadata(metadata_file)
metadata.update(
{
# pylint: disable=consider-using-f-string
"P": "%(name)s-%(version)s" % metadata,
"PN": "%(name)s" % metadata,
"PV": "%(version)s" % metadata,
}
)
metadata.update(
{
"p": metadata["P"].lower(),
"pn": metadata["PN"].lower(),
}
)
# All package-specific build state is under this directory.
basedir = BUILDDIR / opts.toolchain / metadata["p"]
workdir = basedir / "work"
# Package-specific source directory with all the source.
sourcedir = getattr(module, "S", os.path.join(workdir, metadata["p"]))
# Package-specific temp directory.
tempdir = basedir / "temp"
metadata.update(
{
"archives": getattr(module, "ARCHIVES", ()),
"patches": getattr(module, "PATCHES", ()),
"filesdir": filesdir,
"workdir": workdir,
"T": tempdir,
}
)
metadata.update(
{
"S": Path(sourcedir % metadata),
}
)
sourcedir = metadata["S"]
for path in (tempdir, workdir, BUILD_BINDIR, HOME):
os.makedirs(path, exist_ok=True)
toolchain = ToolchainInfo.from_id(opts.toolchain)
toolchain.activate()
metadata["toolchain"] = toolchain
os.environ["HOME"] = str(HOME)
os.environ["PATH"] = os.pathsep.join(
(str(BUILD_BINDIR), os.environ["PATH"])
)
# Run all the source phases now to build it.
common_module = sys.modules[__name__]
def run_phase(phase, cwd):
"""Run this single source phase."""
logging.info(">>> %s: Running phase %s", metadata["P"], phase)
func = getattr(
module, phase, getattr(common_module, f"default_{phase}")
)
os.chdir(cwd)
func(metadata)
run_phase("src_unpack", workdir)
update_gnuconfig(metadata, sourcedir)
run_phase("src_prepare", sourcedir)
run_phase("src_configure", sourcedir)
run_phase("src_compile", sourcedir)
run_phase("src_install", sourcedir)