node/tools/prepare_lief.py
Joyee Cheung 01f335d968
deps: add tools and scripts to pull LIEF as a dependency
PR-URL: https://github.com/nodejs/node/pull/61167
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
2026-01-22 23:32:45 +01:00

310 lines
12 KiB
Python

#!/usr/bin/env python3
import argparse
import re
import shutil
import sys
import tempfile
import zipfile
from pathlib import Path
def ensure_dir(p: Path):
p.mkdir(parents=True, exist_ok=True)
def extract_zip(prefix: str, zip_path: Path, dst_third_party: Path):
"""Extract a third-party archive into dst_third_party/prefix.
Typical structure inside the archive is a single top-level directory like
"mbedtls-<ver>/". We extract under dst_third_party, then rename that
directory to a stable name dst_third_party/prefix.
"""
ensure_dir(dst_third_party)
with zipfile.ZipFile(zip_path, 'r') as zf:
# Determine the top-level directory inside the archive
names = zf.namelist()
top_candidates = {n.split('/', 1)[0] for n in names if '/' in n}
top_dir = next((n for n in sorted(top_candidates) if n.startswith(f"{prefix}-")), None)
if top_dir is None: # There is no versioned top-level dir, create one
top_dir = prefix
extract_parent_dir = dst_third_party / prefix
if not extract_parent_dir.exists():
extract_parent_dir.mkdir()
else:
extract_parent_dir = dst_third_party
print(f"Extracting {zip_path} -> {extract_parent_dir} (top_dir: {top_dir})")
zf.extractall(extract_parent_dir)
extracted_dir = dst_third_party / top_dir
target_dir = dst_third_party / prefix
if extracted_dir.exists() and extracted_dir != target_dir:
if target_dir.exists():
shutil.rmtree(target_dir)
extracted_dir.rename(target_dir)
def find_single_subdir(root: Path) -> Path:
entries = [p for p in root.iterdir() if p.is_dir()]
if len(entries) == 1:
return entries[0]
# Try to pick the most likely one by name length (zip usually extracts into a single dir)
if entries:
return sorted(entries, key=lambda p: len(p.name))[0]
return root
def cmake_configure_in(template: str, values: dict) -> str:
# Handle @VAR@ replacement and #cmakedefine VAR @VAR@
def repl_at(m):
var = m.group(1)
return str(values.get(var, ""))
# Replace @VAR@
s = re.sub(r"@([A-Za-z0-9_]+)@", repl_at, template)
# Process #cmakedefine lines
out_lines = []
for line in s.splitlines():
m = re.match(r"\s*#cmakedefine\s+([A-Za-z0-9_]+)\s+(\S+)", line)
if m:
name = m.group(1)
var_token = m.group(2)
# var_token was already replaced above (may still be a token if not found)
# try to look it up from values if it's still in @VAR@ format
val = 0
if var_token.startswith("@") and var_token.endswith("@"):
key = var_token.strip("@")
val = int(values.get(key, 0) or 0)
else:
try:
val = int(var_token)
except Exception:
val = 0
if val:
out_lines.append(f"#define {name} {val}")
else:
out_lines.append(f"/* #undef {name} */")
else:
out_lines.append(line)
return "\n".join(out_lines) + "\n"
def write_file(path: Path, content: str):
ensure_dir(path.parent)
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
def parse_version_tuple(version: str):
m = re.match(r"^(\d+)\.(\d+)\.(\d+)$", version)
if not m:
return (0, 0, 0)
return tuple(int(x) for x in m.groups())
def main():
ap = argparse.ArgumentParser(description='Prepare LIEF third-party and configured headers for checked-in sources')
ap.add_argument('--source-dir', required=True, help='Path to the unzipped LIEF source directory (e.g. /tmp/LIEF-<ver>/)')
ap.add_argument('--lief-dir', required=True, help='Target path to deps/LIEF (the vendored destination in this repo)')
ap.add_argument('--version', help='LIEF version (X.Y.Z). If omitted, try to infer a fallback (0.17.0).')
ap.add_argument('--commit', default='', help='Commit hash to embed into version.h (optional)')
ap.add_argument('--tag', default='', help='Git tag to embed into version.h (optional)')
args = ap.parse_args()
source_dir = Path(args.source_dir).resolve()
lief_dir = Path(args.lief_dir).resolve()
if not source_dir.exists():
print(f"source-dir does not exist: {source_dir}", file=sys.stderr)
return 1
if not lief_dir.exists():
ensure_dir(lief_dir)
# 1) Copy only the relevant files from source_dir to lief_dir
included_dirs = ['src', 'include', 'config', 'README.md', 'LICENSE']
for item in included_dirs:
src = source_dir / item
dst = lief_dir / item
if not src.exists():
print(f"Warning: expected item missing: {src}", file=sys.stderr)
continue
print(f"Copying {src} -> {dst}")
if src.is_dir():
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
# 2) Extract selected third-party zips into third-party/<base>
# Keep this generic via a small allowlist so we can extend later if needed.
# For now, we only extract 'mbedtls'.
src_third_party = source_dir / 'third-party'
dst_third_party = lief_dir / 'third-party'
ensure_dir(dst_third_party)
# Use a temporary third party directory, then move needed files to lief_dir
tmp_third_party = Path(tempfile.mkdtemp())
# For example, src_third_party/mbedtls-<ver>.zip contains mbedtls-<ver>/,
# we extract that to tmp_third_party/mbedtls, and in the extracted directory, we
# only want to copy the 'library' folder, so tmp_third_party/mbedtls/library ->
# dst_third_party/mbedtls/library. Any other files under the extracted mbedtls
# directory will be ignored.
libs_to_extract = {
'mbedtls': ['library', 'include', 'README.md', 'LICENSE'], # #include "mbedtls/private_access.h"
'spdlog': ['include', 'src', 'LICENSE', 'README.md'], # #include "spdlog/fmt/..."
'frozen': ['include', 'LICENSE', 'README.rst'], # #include <frozen/map.h>
'expected': ['include', 'README.md', 'COPYING'], # #include <LIEF/third-party/internal/expected.hpp>
'utfcpp': ['source', 'README.md', 'LICENSE'], # #include <internal/utfcpp/utf8/unchecked.h> | #include "utf8/unchecked.h"
'tcb-span': ['span.hpp'], # #include <LIEF/third-party/internal/span.hpp>
}
extracted_libs = []
with tempfile.TemporaryDirectory() as _tmp_dir:
tmp_third_party = Path(_tmp_dir)
zip_files = sorted(src_third_party.glob('*.zip'))
for lib, included in libs_to_extract.items():
# Find the matching zip file that starts with lib name
zip_path = next((z for z in zip_files if z.name.startswith(f"{lib}-")), None)
if zip_path is None:
print(f"Warning: No archive found for '{lib}' in {src_third_party}", file=sys.stderr)
continue
# Extract archive into a tmp location under tmp_third_party/{lib}
extract_zip(lib, zip_path, tmp_third_party)
extracted_libs.append(lib)
# Move the allow-listed directories from each lib in tmp_third_party to dst_third_party
src = tmp_third_party / lib
dst = dst_third_party / lib
for subpath in included:
src_item = src / subpath
dst_item = dst / subpath
if not src_item.exists():
print(f"Warning: expected item missing: {src_item}", file=sys.stderr)
continue
ensure_dir(dst_item.parent)
if dst_item.exists():
if dst_item.is_dir():
shutil.rmtree(dst_item)
else:
dst_item.unlink()
print(f"Moving {src_item} -> {dst_item}")
shutil.move(str(src_item), str(dst_item))
# 2) Place internal headers expected by the project layout under src/
# - third-party/expected/include/tl/expected.hpp
# -> include/LIEF/third-party/internal/expected.hpp
# - third-party/tcb-span/span.hpp
# -> include/LIEF/third-party/internal/span.hpp
# - third-party/utfcpp/source/utf8
# -> include/LIEF/third-party/internal/utfcpp/utf8
internal_mappings = [
['third-party/expected/include/tl/expected.hpp', 'include/LIEF/third-party/internal/expected.hpp'],
['third-party/tcb-span/span.hpp', 'include/LIEF/third-party/internal/span.hpp'],
['third-party/utfcpp/source/utf8', 'include/LIEF/third-party/internal/utfcpp/utf8'],
]
for src_rel, dst_rel in internal_mappings:
src_path = lief_dir / src_rel
dst_path = lief_dir / dst_rel
if not src_path.exists():
print(f"Warning: expected internal header missing: {src_path}", file=sys.stderr)
continue
print(f"Copying internal header {src_path} -> {dst_path}")
ensure_dir(dst_path.parent)
if dst_path.exists():
if dst_path.is_dir():
shutil.rmtree(dst_path)
else:
dst_path.unlink()
if src_path.is_dir():
shutil.copytree(src_path, dst_path)
else:
shutil.copy2(src_path, dst_path)
# 3) Generate configured headers into src/LIEF/
# Prefer templates from target include/, fall back to source include/ if missing.
configs = [
['include/LIEF/config.h.in', 'include/LIEF/config.h'],
['include/LIEF/version.h.in', 'include/LIEF/version.h'],
['src/compiler_support.h.in', 'src/compiler_support.h'],
]
# The CMakeLists.txt usually contains the default version values like this. Parse them.
# set(LIEF_VERSION_MAJOR 0)
# set(LIEF_VERSION_MINOR 17)
# set(LIEF_VERSION_PATCH 0)
cmakelists_path = source_dir / 'CMakeLists.txt'
major_ver = minor_ver = patch_ver = 0
if cmakelists_path.exists():
with open(cmakelists_path, 'r', encoding='utf-8') as f:
cmake_contents = f.read()
m_major = re.search(r'set\s*\(\s*LIEF_VERSION_MAJOR\s+(\d+)\s*\)', cmake_contents)
m_minor = re.search(r'set\s*\(\s*LIEF_VERSION_MINOR\s+(\d+)\s*\)', cmake_contents)
m_patch = re.search(r'set\s*\(\s*LIEF_VERSION_PATCH\s+(\d+)\s*\)', cmake_contents)
if m_major:
major_ver = int(m_major.group(1))
if m_minor:
minor_ver = int(m_minor.group(1))
if m_patch:
patch_ver = int(m_patch.group(1))
vals = {
'PROJECT_NAME': 'LIEF',
'LIEF_VERSION_MAJOR': major_ver,
'LIEF_VERSION_MINOR': minor_ver,
'LIEF_VERSION_PATCH': patch_ver,
'LIEF_COMMIT_HASH': args.commit,
'LIEF_IS_TAGGED': 0 if not args.tag else 1,
'LIEF_GIT_TAG': args.tag or '',
'LIEF_PE_SUPPORT': 1,
'LIEF_ELF_SUPPORT': 1,
'LIEF_MACHO_SUPPORT': 1,
'LIEF_COFF_SUPPORT': 1,
'LIEF_OAT_SUPPORT': 0,
'LIEF_DEX_SUPPORT': 0,
'LIEF_VDEX_SUPPORT': 0,
'LIEF_ART_SUPPORT': 0,
'LIEF_DEBUG_INFO_SUPPORT': 0,
'LIEF_OBJC_SUPPORT': 0,
'LIEF_DYLD_SHARED_CACHE_SUPPORT': 0,
'LIEF_ASM_SUPPORT': 0,
'LIEF_EXTENDED': 0,
'ENABLE_JSON_SUPPORT': 0,
'LIEF_JSON_SUPPORT': 0,
'LIEF_LOGGING_SUPPORT': 0,
'LIEF_LOGGING_DEBUG_SUPPORT': 0,
'LIEF_FROZEN_ENABLED': 1,
'LIEF_EXTERNAL_EXPECTED': 0,
'LIEF_EXTERNAL_UTF8CPP': 0,
'LIEF_EXTERNAL_MBEDTLS': 0,
'LIEF_EXTERNAL_FROZEN': 0,
'LIEF_EXTERNAL_SPAN': 0,
'LIEF_NLOHMANN_JSON_EXTERNAL': 0,
'LIEF_SUPPORT_CXX11': 1,
'LIEF_SUPPORT_CXX14': 1,
'LIEF_SUPPORT_CXX17': 1,
}
for cfg_in_rel, cfg_out_rel in configs:
cfg_in_path = lief_dir / cfg_in_rel
cfg_out_path = lief_dir / cfg_out_rel
if not cfg_in_path.exists():
print(f"Warning: config template missing: {cfg_in_path}", file=sys.stderr)
continue
with open(cfg_in_path, 'r', encoding='utf-8') as f:
template = f.read()
configured = cmake_configure_in(template, vals)
ensure_dir(cfg_out_path.parent)
write_file(cfg_out_path, configured)
extracted_summary = ', '.join(sorted(set(extracted_libs))) if extracted_libs else 'none'
print(f'LIEF preparation complete: third-party extracted from {source_dir} into {lief_dir} [{extracted_summary}] and headers configured into src/')
return 0
if __name__ == '__main__':
sys.exit(main())