diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 5984081f3..e3dc772b0 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -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 diff --git a/python/revng/internal/cli/_commands/merge-irs.py b/python/revng/internal/cli/_commands/merge-irs.py new file mode 100644 index 000000000..ca3adecba --- /dev/null +++ b/python/revng/internal/cli/_commands/merge-irs.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()) diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 17115ee40..779830cc5 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -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) diff --git a/tools/merge-llvm/CMakeLists.txt b/tools/merge-llvm/CMakeLists.txt new file mode 100644 index 000000000..3f20b0fbc --- /dev/null +++ b/tools/merge-llvm/CMakeLists.txt @@ -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}) diff --git a/tools/merge-llvm/Main.cpp b/tools/merge-llvm/Main.cpp new file mode 100644 index 000000000..f99b60e88 --- /dev/null +++ b/tools/merge-llvm/Main.cpp @@ -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 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 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; +}