Files
flare/thirdparty/foreign_build.bld
CHEN Feng 6320851cd6 thirdparty: make foreign_build.bld Starlark-parseable (#212)
blade-go's front-end is Starlark, which (unlike Python) has no generator
expressions or implicit string concatenation. Convert foreign_build.bld to the
equivalent Starlark forms so it can be include()'d there, with no behavior change
under Python Blade:

- generator expr -> list comprehension in the rpath ' '.join(...).
- adjacent-string-literal concatenation -> explicit '+' in dyld_path, the
  autotools configure command, and the cmake generate command (parenthesized so
  the trailing `% (...)` still applies to the whole concatenation).
- fix cmake_cc_library, which was dead-but-unparseable: it referenced undefined
  package_name/deps/with_packages. Give it deps/with_packages params, pass
  package_name=name and source_package through (mirrors autotools_cc_library).

Same Starlark-compat class as the cc_flare_library change in the build_rules.bld.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:23:35 +08:00

430 lines
19 KiB
Python

# vim: filetype=python
"""
This file defines rules to build from source package with foreign build tools,
such as `GNU Autotools(autoconf, automake)`, CMake.
`autotools_build` is used to build autotools-like packages.
`cmake_build` is used to build CMake packages.
`autotools_cc_library` and `cmake_cc_library` are used to build source packages
which contains only one library, if there are more than one libraries in one
source package, you should use `autotools_build` or `cmake_build` to build the
package, and then use `foreign_cc_library` to describe each libraries.
Normally, we suggest put each foreign package into one subdirectory, such as put
`openssl` into `//thirdparty/openssl` directory.
"""
# TODO:
# cmake ninja
# ccache
# optimize with_packages
# static/dynamic library control
# automatic jobs
_BLADE_UNPACK_STAMP = 'BLADE_UNPACK_STAMP'
# Provide a pathed include path for each header file in the installed include dir.
# If you want to debug this code snippet, add '; false' before the last ')'
_EXPORT_HEADERS = """\
(cd {install_dir} && \
for f in $$(find {include_dir} -name '*.h' -or -name '*.hpp'); do \
nf=$${{f#{include_dir}/}}; \
d=$$(dirname $$nf); \
if [ "$$d" != "." ]; then \
mkdir -p "$$d"; \
fi; \
echo "#include \\\"{install_dir}/$$f\\\"" > $$nf; \
done)"""
def _get_package_name(pkg):
"""Get software package name from archive file name"""
for ext in ('tar.gz', 'tar.bz2', 'tar.xz', 'tar.lz', 'tgz', 'tbz', 'zip'):
if pkg.endswith(ext):
return pkg[:-(len(ext) + 1)]
return ''
def _cmd_with_log(cmd, log_file):
"""Make cmd run silently if success else show log"""
# NOTE: the inner parenthesis is necessary because bash is left associative
return '({cmd} > {log_file} 2>&1 || (cat {log_file} && false))'.format(
cmd=cmd, log_file=log_file)
def _expand_libnames(install_dir, lib_dir, lib_names, generate_dynamic):
"""Expand library names to library filenames"""
result = []
dyn_suffix = blade.cc_toolchain.dynamic_lib_suffix
for lib_name in lib_names:
if isinstance(lib_name, tuple):
libprefix = blade.path.join(lib_name[0], 'lib' + lib_name[1])
else:
libprefix = 'lib' + lib_name
libprefix = blade.path.join(install_dir, lib_dir, libprefix)
result.append(libprefix + '.a')
if generate_dynamic:
result.append(libprefix + dyn_suffix)
return result
def _unpack_source_package(name, source_package, source_dir, patches):
"""Unpack a source_package to the target build directory"""
full_source_dir = blade.path.join(blade.current_target_dir(), source_dir)
stamp_file = blade.path.join(source_dir, _BLADE_UNPACK_STAMP)
log_file = blade.path.join(blade.current_target_dir(), name + '.log')
if source_package.endswith('.zip'):
unpack_cmd = 'unzip -o -d $OUT_DIR $FIRST_SRC'
else:
unpack_cmd = 'tar xf $FIRST_SRC -C $OUT_DIR'
patch_files = [
blade.path.join(blade.current_source_dir(), x) for x in patches
]
cmds = [
_cmd_with_log(unpack_cmd, log_file),
] + [
_cmd_with_log('patch -p1 -d ' + full_source_dir + ' < ' + x, log_file)
for x in patch_files
] + [
'touch $OUTS',
]
gen_rule(name=name,
srcs=[source_package] + patches,
outs=stamp_file,
cmd=' && '.join(cmds),
cmd_name='UNPACK')
def _configure(name, package_name, source_dir, install_dir, with_packages, deps,
configure_options, configure_file_name, ld_library_path):
# Some packages use their homemake configure which doesn't generate Makefile.
# Force touch it to avoid always rebuilding.
# The doubled '$' is required to avoid early expansion
full_install_dir = blade.path.abspath(
blade.path.join(blade.current_target_dir(), install_dir))
lib_path_env = ':'.join(ld_library_path)
# Build rpath flags so that runtime library checks (e.g. curl's configure)
# can find the just-built shared libs without LD_LIBRARY_PATH.
rpath_flags = ' '.join(['-Wl,-rpath,' + p for p in ld_library_path])
is_darwin = blade.cc_toolchain.target_os == 'darwin'
dyld_path = (('DYLD_LIBRARY_PATH=%s:$DYLD_LIBRARY_PATH ' +
'DYLD_FALLBACK_LIBRARY_PATH=%s:$DYLD_FALLBACK_LIBRARY_PATH ') % (
lib_path_env, lib_path_env)) if is_darwin else ''
# Pass CC/CXX explicitly from the resolved toolchain rather than letting
# autoconf pick from $CC/$CXX or its own detection. Matches the cmake
# path above and keeps the thirdparty layer in sync with whatever
# blade.cc_toolchain.tool() decided at config time.
cc = blade.cc_toolchain.tool('cc') or 'cc'
cxx = blade.cc_toolchain.tool('cxx') or 'c++'
# Pin the archiver too. blade.cc_toolchain.tool('ar') is the bare name `ar`,
# so an autotools package falls back to whatever `ar`/`ranlib` is first in
# PATH. On macOS a stray GNU/LLVM `ar` (e.g. from Homebrew binutils) there
# produces a GNU-format archive -- with `/` and `//` index members -- that
# Apple's ld then rejects at link time ("archive member '/' not a mach-o
# file"). Force the system BSD ar/ranlib, which emit a `__.SYMDEF` archive
# ld accepts. Off macOS, GNU archives are fine, so honor the toolchain ar.
ar = '/usr/bin/ar' if is_darwin else (blade.cc_toolchain.tool('ar') or 'ar')
ranlib = '/usr/bin/ranlib' if is_darwin else 'ranlib'
configure = ('cd $OUT_DIR/%s && ' +
'LD_LIBRARY_PATH=%s:$LD_LIBRARY_PATH ' +
'%s' +
'CC=%s CXX=%s AR=%s RANLIB=%s ' +
'LDFLAGS="$LDFLAGS %s" ' +
'./%s --prefix=%s %s AR=%s RANLIB=%s') % (
source_dir,
lib_path_env,
dyld_path,
cc, cxx, ar, ranlib,
rpath_flags,
configure_file_name,
full_install_dir,
configure_options,
ar, ranlib)
if with_packages:
for pkg in with_packages:
pkg = pkg[1:] # remove prefix ':'
configure += ' --with-%s=$$PWD/../%s' % (pkg, pkg)
log_file = blade.path.join(blade.current_target_dir(), name + '.log')
makefile = blade.path.join(source_dir, 'Makefile')
cmds = [
_cmd_with_log('(%s)' % configure, log_file),
'touch $OUT_DIR/%s' % makefile
]
gen_rule(name=name,
srcs=[blade.path.join(source_dir, _BLADE_UNPACK_STAMP)],
outs=[makefile],
cmd=' && '.join(cmds),
cmd_name='CONFIGURE',
deps=deps + with_packages)
def autotools_build(name,
source_package,
package_name,
lib_names,
install_dir='',
source_dir=None,
with_packages=[],
include_dir='include',
strip_include_prefix='',
deps=[],
configure_options="",
configure_file_name="configure",
install_target="install",
generate_dynamic=False,
patches=[],
ld_library_path=[]):
"""Build a autotools(also known as the GNU build system) source package.
Args:
name: str, the name of the target, suggest based on the package name with a '_build' suffix
source_package: str, filename of the source_package, support zip and 'tar.*z'
package_name: str, name of the package, without version number.
lib_names: List[str], if the package generates multiple libraries, list them here, without the `lib` prefix
source_package: Optional[str], dir name of the extracted source package, usually same as
the package_name without the compression suffix.
with_packages: List[str], names to be passed to configure --with-xxx
"""
source_dir = source_dir or _get_package_name(source_package)
target_unpack = package_name + '_unpack'
_unpack_source_package(name=target_unpack,
source_package=source_package,
source_dir=source_dir,
patches=patches)
target_configure = package_name + '_configure'
_configure(name=target_configure,
package_name=package_name,
install_dir=install_dir,
source_dir=source_dir,
with_packages=with_packages,
deps=[':' + target_unpack] + deps,
configure_options=configure_options,
configure_file_name=configure_file_name,
ld_library_path=ld_library_path)
full_install_dir = blade.path.join(blade.current_target_dir(), install_dir)
build_outs = _expand_libnames(install_dir, 'lib', lib_names,
generate_dynamic)
log_file = blade.path.join(blade.current_target_dir(),
package_name + '_make.log')
build_cmd = '(make -C {dir} -j8 && make -C {dir} {install_target})'.format(
dir=blade.path.join('$OUT_DIR', source_dir),
install_target=install_target)
strip_include_dir = blade.path.normpath(
blade.path.join(include_dir, strip_include_prefix))
cmds = [
_cmd_with_log(build_cmd, log_file),
_EXPORT_HEADERS.format(install_dir=full_install_dir,
include_dir=strip_include_dir),
]
gen_rule(name=name,
srcs=source_package,
outs=build_outs,
cmd=' && '.join(cmds),
cmd_name='MAKE',
deps=deps + with_packages + [':' + target_configure],
generated_incs='',
# Third-party install include dirs go via `-isystem` so that
# warnings raised inside vendored headers (glog self-deprecation,
# openssl pedantic notes, etc.) don't fail our `-Werror` build.
# Requires blade 5b305b0+ (system_export_incs gen_rule param).
system_export_incs=blade.path.join(install_dir, include_dir),
heavy=True)
def autotools_cc_library(name,
source_package,
source_dir=None,
lib_name=None,
with_packages=[],
include_dir='include',
deps=[],
autogen=False,
enables=[],
verbose=False):
target_build = name + '_build'
autotools_build(name=target_build,
source_package=source_package,
package_name=name,
lib_names=[name],
source_dir=source_dir,
with_packages=with_packages,
include_dir=include_dir)
install_dir = blade.current_target_dir()
foreign_cc_library(name=name,
package_name='',
deps=[':' + target_build] + with_packages + deps,
export_incs='//' +
blade.path.join(install_dir, 'include'))
def _cmake_generate(name, package_name, source_dir, install_dir, deps, options):
"""Call cmake to generate build scripts"""
# Many cmake package forbid in source build, so we build all cmake package out of source.
build_dir_name = package_name + '_build'
build_dir = blade.path.join(blade.current_target_dir(), build_dir_name)
# install_dir relative to the cmake build dir (= $OUT_DIR/<build_dir_name>)
install_dir = blade.path.join('..', install_dir)
# CMake needs an absolute prefix to generate an absolute install_name on
# macOS; a relative path produces just the filename. install_dir is always
# ".." or "../foo": strip the leading "../" and join with the target dir.
full_install_dir = blade.path.abspath(
blade.path.join(blade.current_target_dir(), install_dir[3:]))
log_file = '$OUT_DIR/%s.log' % name
cmake_options = ' '.join([
'-DCMAKE_INSTALL_PREFIX=%s' % full_install_dir,
'-DCMAKE_INSTALL_LIBDIR=lib',
# Use absolute install names on macOS (matching autotools behavior)
# instead of @rpath, so consumers don't need per-target rpath setup.
'-DCMAKE_MACOSX_RPATH=OFF',
'-DCMAKE_INSTALL_NAME_DIR=%s' % blade.path.join(full_install_dir, 'lib'),
# Do NOT use user's cmake package registry.
'-DCMAKE_EXPORT_NO_PACKAGE_REGISTRY=ON',
'-DCMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY=ON',
'-DCMAKE_FIND_PACKAGE_NO_SYSTEM_PACKAGE_REGISTRY=ON',
'-DCMAKE_EXPORT_PACKAGE_REGISTRY=OFF',
'-DCMAKE_FIND_USE_PACKAGE_REGISTRY=OFF',
'-DCMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY=OFF',
# CMake 4.x dropped compatibility with `cmake_minimum_required(<3.5)`,
# which several old vendored packages still declare. Allow them to
# configure (a no-op on the 3.x toolchains used elsewhere).
'-DCMAKE_POLICY_VERSION_MINIMUM=3.5',
])
if options:
cmake_options += ' '
cmake_options += ' '.join(options)
# Read the compiler from the resolved toolchain rather than hard-coding
# `CC=gcc CXX=g++`. blade has already picked the toolchain at config time
# (BLADE_ROOT cc_toolchain_config or its fallback to shutil.which); using
# toolchain.tool() keeps the cmake subbuild in sync with the rest of the
# build instead of always invoking the distro-default gcc/g++. The
# fallback strings are only used if the toolchain proxy returns None
# (which shouldn't happen for cc/cxx, but is the documented contract).
cc = blade.cc_toolchain.tool('cc') or 'cc'
cxx = blade.cc_toolchain.tool('cxx') or 'c++'
# On macOS, the host compiler defaults to the SDK minimum deployment target
# (e.g. 15.0), while cmake auto-detects the full OS version (e.g. 15.7).
# Align cmake to the compiler's default to avoid linker warnings.
cmd = ' && '.join([
'rm -fr {build_dir}', 'mkdir -p {build_dir}', 'cd {build_dir}',
'CXX={cxx} CC={cc} cmake {options}' +
' $$(if [ "$$(uname)" = "Darwin" ]; then' +
' echo "-DCMAKE_OSX_DEPLOYMENT_TARGET=$$(sw_vers -productVersion | cut -d. -f1).0";' +
' fi) ../{source_dir}'
]).format(build_dir=build_dir, options=cmake_options,
source_dir=source_dir, cc=cc, cxx=cxx)
build_file = blade.path.join(build_dir_name, 'Makefile')
gen_rule(name=name,
srcs=[blade.path.join(source_dir, _BLADE_UNPACK_STAMP)],
outs=[build_file],
cmd=_cmd_with_log('(%s)' % cmd, log_file),
cmd_name='CMAKE GENERATE',
deps=deps)
return build_dir, build_file
def cmake_build(name,
package_name,
source_package,
source_dir=None,
deps=[],
install_dir='',
lib_dir='lib',
lib_names=None,
include_dir='include',
strip_include_prefix='',
cmake_options=None,
generate_dynamic=False,
shared_cmake_options=None,
patches=[]):
source_dir = source_dir or _get_package_name(source_package)
lib_names = lib_names or [package_name]
target_unpack = package_name + '_unpack'
_unpack_source_package(name=target_unpack,
source_package=source_package,
source_dir=source_dir,
patches=patches)
target_generate = package_name + '_generate'
cmake_options = cmake_options or []
build_dir, build_file = _cmake_generate(name=target_generate,
package_name=package_name,
source_dir=source_dir,
install_dir=install_dir,
deps=[':' + target_unpack] + deps,
options=cmake_options)
build_outs = _expand_libnames(install_dir, lib_dir, lib_names,
generate_dynamic)
log_file = blade.path.join(blade.current_target_dir(), name + '.log')
build_cmd = 'make -C {dir} -j16 install'.format(dir=build_dir)
full_install_dir = blade.path.join(blade.current_target_dir(), install_dir)
strip_include_dir = blade.path.normpath(
blade.path.join(include_dir, strip_include_prefix))
src_files = [build_file]
generate_deps = [':' + target_generate]
cmds = [_cmd_with_log(build_cmd, log_file)]
# Packages whose CMake can only emit static *or* shared in a single
# configure (e.g. googletest, which keys solely off BUILD_SHARED_LIBS)
# need a second pass to produce both. `shared_cmake_options` triggers an
# extra configure+install into the SAME prefix with those extra options
# appended; the static pass installs the `.a`, the shared pass the
# dynamic lib, and headers are installed by both (idempotent). Other
# `generate_dynamic` packages emit both natively and leave this unset.
if shared_cmake_options:
shared_build_dir, shared_build_file = _cmake_generate(
name=package_name + '_generate_shared',
package_name=package_name + '_shared',
source_dir=source_dir,
install_dir=install_dir,
deps=[':' + target_unpack] + deps,
options=cmake_options + shared_cmake_options)
src_files.append(shared_build_file)
generate_deps.append(':' + package_name + '_generate_shared')
cmds.append(_cmd_with_log(
'make -C {dir} -j16 install'.format(dir=shared_build_dir),
log_file))
cmds.append(_EXPORT_HEADERS.format(install_dir=full_install_dir,
include_dir=strip_include_dir))
gen_rule(name=name,
srcs=src_files,
outs=build_outs,
cmd=' && '.join(cmds),
cmd_name='CMAKE BUILD',
deps=deps + generate_deps,
generated_incs=package_name,
# Same rationale as autotools_build above: third-party install
# include dirs go via `-isystem` (blade 5b305b0+).
system_export_incs=blade.path.join(install_dir, include_dir),
heavy=True)
def cmake_cc_library(name,
source_package,
source_dir=None,
out_of_source_build=True,
install_dir='',
include_dir='include',
strip_include_prefix='',
deps=[],
with_packages=[],
verbose=False):
target_build = name + '_build'
cmake_build(name=target_build,
package_name=name,
source_package=source_package,
source_dir=source_dir,
deps=deps,
lib_names=[name],
include_dir=include_dir,
strip_include_prefix=strip_include_prefix)
foreign_cc_library(name=name,
package_name=name,
deps=[':' + target_build] + with_packages + deps,
export_incs=blade.path.join(install_dir, include_dir))
def get_install_dir(name):
return blade.path.join(
blade.path.dirname(blade.path.abspath(blade.current_target_dir())),
name)