blob: b285448693352fc57691972b4ff85ddad97c0a53 [file] [log] [blame]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2018 The Chromium OS Authors. All rights reserved.
# 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."""
from __future__ import print_function
import logging
import multiprocessing
import os
import re
import shutil
import sys
BIN_DIR = os.path.dirname(os.path.realpath(__file__))
DIR = os.path.dirname(BIN_DIR)
LIBAPPS_DIR = os.path.dirname(DIR)
sys.path.insert(0, os.path.join(LIBAPPS_DIR, 'libdot', 'bin'))
import libdot # pylint: disable=wrong-import-position
# The top output directory. Everything lives under this.
OUTPUT = os.path.join(DIR, 'output')
# Where archives are cached for fetching & unpacking.
DISTDIR = os.path.join(OUTPUT, 'distfiles')
# All package builds happen under this path.
BUILDDIR = os.path.join(OUTPUT, 'build')
# Directory to put build-time tools.
BUILD_BINDIR = os.path.join(OUTPUT, 'bin')
# Some tools like to scribble in $HOME.
HOME = os.path.join(OUTPUT, 'home')
# Where we save shared libs and headers.
SYSROOT = os.path.join(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)
shutil.copy(source, dest)
def emake(*args, **kwargs):
"""Run `make` with |args| and automatic -j."""
jobs = kwargs.pop('jobs', JOBS)
run(['make', '-j%s' % (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, os.path.join(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, '.stamp.%s.%s' % (phase, unique))
def unpack(archive, cwd=None, workdir=None):
"""Unpack |archive| into |cwd|."""
distfile = os.path.join(DISTDIR, archive)
stamp = stamp_name(workdir, 'unpack', os.path.basename(archive))
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 = self._env['SYSROOT']
self.libdir = os.path.join(self.sysroot, 'lib')
self.incdir = os.path.join(self.sysroot, 'include')
self.pkgconfdir = os.path.join(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 = os.path.join(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 = os.path.join(OUTPUT, 'naclsdk')
toolchain_root = os.path.join(nacl_sdk_root, 'toolchain', 'linux_pnacl')
bin_dir = os.path.join(toolchain_root, 'bin')
compiler_prefix = os.path.join(bin_dir, 'pnacl-')
sysroot = os.path.join(toolchain_root, 'le32-nacl')
sysroot_libdir = os.path.join(sysroot, 'lib')
pkgconfig_dir = os.path.join(sysroot_libdir, 'pkgconfig')
return {
'CHOST': 'nacl',
'NACL_ARCH': 'pnacl',
'NACL_SDK_ROOT': nacl_sdk_root,
'PATH': os.path.sep.join((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': pkgconfig_dir,
'PKG_CONFIG_LIBDIR': sysroot_libdir,
'SYSROOT': sysroot,
'CPPFLAGS': '-I%s -I%s' % (
os.path.join(sysroot, 'include', 'glibc-compat'),
os.path.join(nacl_sdk_root, 'include')),
'LDFLAGS': '-L%s' % (os.path.join(nacl_sdk_root, 'lib', 'pnacl',
'Release'),),
}
def _toolchain_wasm_env():
"""Get custom env to build using WASM toolchain."""
sdk_root = os.path.join(OUTPUT, 'wasi-sdk')
bin_dir = os.path.join(sdk_root, 'bin')
sysroot = os.path.join(sdk_root, 'share', 'wasi-sysroot')
libdir = os.path.join(sysroot, 'lib')
incdir = os.path.join(sysroot, 'include')
pcdir = os.path.join(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': os.path.join(bin_dir, 'clang') + ' --sysroot=%s' % sysroot,
'CXX': os.path.join(bin_dir, 'clang++') + ' --sysroot=%s' % sysroot,
'AR': os.path.join(bin_dir, 'llvm-ar'),
'RANLIB': os.path.join(bin_dir, 'llvm-ranlib'),
'STRIP': os.path.join(BUILD_BINDIR, 'wasm-strip'),
'PKG_CONFIG_SYSROOT_DIR': sysroot,
'PKG_CONFIG_LIBDIR': pcdir,
'SYSROOT': sysroot,
'CPPFLAGS': '-isystem %s' % (os.path.join(incdir, 'wassh-libc-sup'),),
'LDFLAGS': ' '.join([
'-L%s' % (libdir,),
'-lwassh-libc-sup',
'-Wl,--allow-undefined-file=%s' % (
os.path.join(libdir, 'wassh-libc-sup.imports'),),
'-Wl,--export=__wassh_signal_handlers',
]),
}
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 = os.path.join(filesdir, patch)
logging.info('Applying patch %s', name)
run(['patch', '-p1'], stdin=open(patch, 'rb'))
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 = os.path.join(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({
'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 = os.path.join(BUILDDIR, opts.toolchain, metadata['p'])
workdir = os.path.join(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 = os.path.join(basedir, 'temp')
metadata.update({
'archives': getattr(module, 'ARCHIVES', ()),
'patches': getattr(module, 'PATCHES', ()),
'filesdir': filesdir,
'workdir': workdir,
'T': tempdir,
})
metadata.update({
'S': 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'] = HOME
os.environ['PATH'] = os.pathsep.join((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, 'default_%s' % 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)