Add revng merge llvm command

Add the `revng merge llvm` command which allows linking multiple LLVM
IR modules together into a single module.
This commit is contained in:
Giacomo Vercesi
2026-05-19 09:58:08 +02:00
committed by Alessandro Di Federico
parent 5287e69be0
commit 49b2c523ab
5 changed files with 161 additions and 0 deletions
+1
View File
@@ -351,6 +351,7 @@ set(REVNG_CLI_COMMANDS_MODULE_FILES
revng/internal/cli/_commands/test_docs/__init__.py
revng/internal/cli/_commands/test_docs/doctest_runner.py
revng/internal/cli/_commands/trace_run.py
revng/internal/cli/_commands/merge-irs.py
revng/internal/cli/_commands/model_compare.py
revng/internal/cli/_commands/model_migrate.py
revng/internal/cli/_commands/model_export_sqlite.py
@@ -0,0 +1,96 @@
#
# This file is distributed under the MIT License. See LICENSE.md for details.
#
import tarfile
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from base64 import b64decode
from contextlib import suppress
from pathlib import Path
from shutil import copyfileobj
from tempfile import TemporaryDirectory, TemporaryFile
from typing import IO
import yaml
from revng.internal.cli.commands_registry import Command, CommandsRegistry, Options
from revng.internal.cli.support import build_command_with_loads, file_wrapper, try_run
from revng.support import TarDictionary
def read_single_file(input_: IO[bytes]) -> list[bytes]:
# If we encounter a non-seekable file (e.g. stdin) first copy it to a
# temporary file and then re-run this function
if not input_.seekable():
with TemporaryFile() as temp_file:
copyfileobj(input_, temp_file)
temp_file.seek(0)
return read_single_file(temp_file)
# If here, the file is seekable, test the various formats by trying to read
# them and moving onto the next format if the previous throws an error
# Test for YAML
with suppress(yaml.YAMLError):
data = yaml.load(input_, Loader=yaml.CSafeLoader)
return [b64decode(v) for v in data.values()]
# Test for tar
input_.seek(0)
with suppress(tarfile.ReadError):
return list(TarDictionary(input_).values())
# No good format found, assume it's a single plain file
input_.seek(0)
return [input_.read()]
class MergeLLVMIRCommand(Command):
def __init__(self):
super().__init__(("merge", "llvm"), "Merge multiple LLVM IR files into a single one")
def register_arguments(self, parser: ArgumentParser):
parser.formatter_class = RawDescriptionHelpFormatter
parser.description = """Links multiple LLVM IR files into a single one.
This program works differently depending on the number of inputs:
* If one input is passed it is interpreted as either:
* a YAML dict with base64-encoded values
* a tar where each file is a bitcode file
* a single LLVM module
* If no input is passed, treat stdin as a single input
* If multiple input files are passed, treat all of them as bitcode files
"""
parser.add_argument("-o", "--output", help="Where to output the file, stdout if omitted")
parser.add_argument("inputs", nargs="*", help="Input file(s), omit to use stdin")
def run(self, options: Options):
args = options.parsed_args
if len(args.inputs) >= 2:
return self.run_merge(args.inputs, options)
input_file = args.inputs[0] if len(args.inputs) == 1 else "-"
with file_wrapper(input_file, "rb") as input_:
data = read_single_file(input_)
with TemporaryDirectory(prefix="tmp.revng-merge-llvm.") as temp_dir:
temp_dir_path = Path(temp_dir)
input_paths = []
for index, value in enumerate(data):
element_path = temp_dir_path / str(index)
element_path.write_bytes(value)
input_paths.append(str(element_path.resolve()))
return self.run_merge(input_paths, options)
def run_merge(self, inputs: list[str], options: Options):
with file_wrapper(options.parsed_args.output, "wb") as output_file:
return try_run(
build_command_with_loads("merge-llvm", inputs, options),
options,
stdout=output_file,
)
def setup(commands_registry: CommandsRegistry):
commands_registry.define_namespace(("merge",), "Tools to merge function-wise object together")
commands_registry.register_command(MergeLLVMIRCommand())
+1
View File
@@ -5,6 +5,7 @@
add_subdirectory(clift-opt)
add_subdirectory(internal)
add_subdirectory(lddtree)
add_subdirectory(merge-llvm)
add_subdirectory(model)
add_subdirectory(pipeline)
add_subdirectory(pypeline)
+7
View File
@@ -0,0 +1,7 @@
#
# This file is distributed under the MIT License. See LICENSE.md for details.
#
revng_add_executable(merge-llvm Main.cpp)
target_link_libraries(merge-llvm revngSupport ${LLVM_LIBRARIES})
+56
View File
@@ -0,0 +1,56 @@
//
// This file is distributed under the MIT License. See LICENSE.md for details.
//
#include "llvm/IR/Module.h"
#include "llvm/IRReader/IRReader.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/PluginLoader.h"
#include "llvm/Support/SourceMgr.h"
#include "revng/Support/CommandLine.h"
#include "revng/Support/Error.h"
#include "revng/Support/IRHelpers.h"
#include "revng/Support/InitRevng.h"
#include "revng/Support/Tar.h"
static llvm::cl::list<std::string> Arguments(llvm::cl::Positional,
llvm::cl::ZeroOrMore,
llvm::cl::desc("FILES"),
llvm::cl::cat(MainCategory));
static llvm::ExitOnError AbortOnError;
int main(int argc, char *argv[]) {
revng::InitRevng X(argc, argv, "", { &MainCategory });
llvm::LLVMContext Context;
std::unique_ptr<llvm::Module> OutputModule;
for (llvm::StringRef Filename : Arguments) {
llvm::SMDiagnostic Err;
auto Module = llvm::parseIRFile(Filename, Err, Context);
revng_assert(Module != nullptr);
if (OutputModule != nullptr) {
// Make the existing module be linked to the new module, this allows
// preserving the order of definitions as the order of the supplied files
std::swap(Module, OutputModule);
linkFunctionModules(std::move(Module), OutputModule);
} else {
OutputModule = std::move(Module);
}
}
revng_assert(OutputModule != nullptr);
revng::verify(OutputModule.get());
{
std::error_code EC;
llvm::raw_fd_ostream OS("-", EC);
revng_assert(not EC);
OutputModule->print(OS, nullptr);
}
return EXIT_SUCCESS;
}