Skip to content

Commit 9c8c688

Browse files
ryanofskySjors
andcommitted
multiprocess: Add bitcoin wrapper executable
Intended to make bitcoin command line features more discoverable and allow installing new multiprocess binaries in libexec/ instead of bin/ so they don't cause confusion. Idea and implementation of this were discussed in bitcoin/bitcoin#30983 Co-authored-by: Sjors Provoost <[email protected]>
1 parent 5076d20 commit 9c8c688

File tree

4 files changed

+216
-0
lines changed

4 files changed

+216
-0
lines changed

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ endif()
9494
#=============================
9595
include(CMakeDependentOption)
9696
# When adding a new option, end the <help_text> with a full stop for consistency.
97+
option(BUILD_BITCOIN_BIN "Build bitcoin executable." ON)
9798
option(BUILD_DAEMON "Build bitcoind executable." ON)
9899
option(BUILD_GUI "Build bitcoin-qt executable." OFF)
99100
option(BUILD_CLI "Build bitcoin-cli executable." ON)
@@ -202,6 +203,7 @@ target_link_libraries(core_interface INTERFACE
202203

203204
if(BUILD_FOR_FUZZING)
204205
message(WARNING "BUILD_FOR_FUZZING=ON will disable all other targets and force BUILD_FUZZ_BINARY=ON.")
206+
set(BUILD_BITCOIN_BIN OFF)
205207
set(BUILD_DAEMON OFF)
206208
set(BUILD_CLI OFF)
207209
set(BUILD_TX OFF)
@@ -643,6 +645,7 @@ message("\n")
643645
message("Configure summary")
644646
message("=================")
645647
message("Executables:")
648+
message(" bitcoin ............................. ${BUILD_BITCOIN_BIN}")
646649
message(" bitcoind ............................ ${BUILD_DAEMON}")
647650
if(BUILD_DAEMON AND ENABLE_IPC)
648651
set(bitcoin_daemon_status ON)

contrib/guix/security-check.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ def check_ELF_FORTIFY(binary) -> bool:
126126
# bitcoin-util does not currently contain any fortified functions
127127
if 'Bitcoin Core bitcoin-util utility version ' in binary.strings:
128128
return True
129+
# bitcoin wrapper does not currently contain any fortified functions
130+
if '--monolithic' in binary.strings:
131+
return True
129132

130133
chk_funcs = set()
131134

src/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,12 @@ target_link_libraries(bitcoin_node
331331
$<TARGET_NAME_IF_EXISTS:USDT::headers>
332332
)
333333

334+
# Bitcoin wrapper executable that can call other executables.
335+
if(BUILD_BITCOIN_BIN)
336+
add_executable(bitcoin bitcoin.cpp)
337+
target_link_libraries(bitcoin core_interface bitcoin_util)
338+
install_binary_component(bitcoin)
339+
endif()
334340

335341
# Bitcoin Core bitcoind.
336342
if(BUILD_DAEMON)

src/bitcoin.cpp

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
// Copyright (c) 2025 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <bitcoin-build-config.h> // IWYU pragma: keep
6+
7+
#include <clientversion.h>
8+
#include <util/fs.h>
9+
#include <util/exec.h>
10+
#include <util/strencodings.h>
11+
#include <util/translation.h>
12+
13+
#include <iostream>
14+
#include <string>
15+
#include <tinyformat.h>
16+
#include <vector>
17+
18+
const TranslateFn G_TRANSLATION_FUN{nullptr};
19+
20+
static constexpr auto HELP_USAGE = R"(Usage: %s [OPTIONS] COMMAND...
21+
22+
Options:
23+
-m, --multiprocess Run multiprocess binaries bitcoin-node, bitcoin-gui.
24+
-M, --monolithic Run monolithic binaries bitcoind, bitcoin-qt. (Default behavior)
25+
-v, --version Show version information
26+
-h, --help Show this help message
27+
28+
Commands:
29+
gui [ARGS] Start GUI, equivalent to running 'bitcoin-qt [ARGS]' or 'bitcoin-gui [ARGS]'.
30+
node [ARGS] Start node, equivalent to running 'bitcoind [ARGS]' or 'bitcoin-node [ARGS]'.
31+
rpc [ARGS] Call RPC method, equivalent to running 'bitcoin-cli -named [ARGS]'.
32+
wallet [ARGS] Call wallet command, equivalent to running 'bitcoin-wallet [ARGS]'.
33+
tx [ARGS] Manipulate hex-encoded transactions, equivalent to running 'bitcoin-tx [ARGS]'.
34+
help [-a] Show this help message. Include -a or --all to show additional commands.
35+
)";
36+
37+
static constexpr auto HELP_EXTRA = R"(
38+
Additional less commonly used commands:
39+
bench [ARGS] Run bench command, equivalent to running 'bench_bitcoin [ARGS]'.
40+
chainstate [ARGS] Run bitcoin kernel chainstate util, equivalent to running 'bitcoin-chainstate [ARGS]'.
41+
test [ARGS] Run unit tests, equivalent to running 'test_bitcoin [ARGS]'.
42+
test-gui [ARGS] Run GUI unit tests, equivalent to running 'test_bitcoin-qt [ARGS]'.
43+
)";
44+
45+
struct CommandLine {
46+
bool use_multiprocess{false};
47+
bool show_version{false};
48+
bool show_help{false};
49+
bool show_help_all{false};
50+
std::string_view command;
51+
std::vector<const char*> args;
52+
};
53+
54+
CommandLine ParseCommandLine(int argc, char* argv[]);
55+
static void ExecCommand(const std::vector<const char*>& args, std::string_view argv0);
56+
57+
int main(int argc, char* argv[])
58+
{
59+
try {
60+
CommandLine cmd{ParseCommandLine(argc, argv)};
61+
if (cmd.show_version) {
62+
tfm::format(std::cout, "%s version %s\n%s", CLIENT_NAME, FormatFullVersion(), FormatParagraph(LicenseInfo()));
63+
return EXIT_SUCCESS;
64+
}
65+
66+
std::vector<const char*> args;
67+
if (cmd.show_help || cmd.command.empty()) {
68+
tfm::format(std::cout, HELP_USAGE, argv[0]);
69+
if (cmd.show_help_all) tfm::format(std::cout, HELP_EXTRA);
70+
return cmd.show_help ? EXIT_SUCCESS : EXIT_FAILURE;
71+
} else if (cmd.command == "gui") {
72+
args.emplace_back(cmd.use_multiprocess ? "bitcoin-gui" : "bitcoin-qt");
73+
} else if (cmd.command == "node") {
74+
args.emplace_back(cmd.use_multiprocess ? "bitcoin-node" : "bitcoind");
75+
} else if (cmd.command == "rpc") {
76+
args.emplace_back("bitcoin-cli");
77+
// Since "bitcoin rpc" is a new interface that doesn't need to be
78+
// backward compatible, enable -named by default so it is convenient
79+
// for callers to use a mix of named and unnamed parameters. Callers
80+
// can override this by specifying -nonamed, but should not need to
81+
// unless they are passing string values containing '=' characters
82+
// as unnamed parameters.
83+
args.emplace_back("-named");
84+
} else if (cmd.command == "wallet") {
85+
args.emplace_back("bitcoin-wallet");
86+
} else if (cmd.command == "tx") {
87+
args.emplace_back("bitcoin-tx");
88+
} else if (cmd.command == "bench") {
89+
args.emplace_back("bench_bitcoin");
90+
} else if (cmd.command == "chainstate") {
91+
args.emplace_back("bitcoin-chainstate");
92+
} else if (cmd.command == "test") {
93+
args.emplace_back("test_bitcoin");
94+
} else if (cmd.command == "test-gui") {
95+
args.emplace_back("test_bitcoin-qt");
96+
} else if (cmd.command == "util") {
97+
args.emplace_back("bitcoin-util");
98+
} else {
99+
throw std::runtime_error(strprintf("Unrecognized command: '%s'", cmd.command));
100+
}
101+
if (!args.empty()) {
102+
args.insert(args.end(), cmd.args.begin(), cmd.args.end());
103+
ExecCommand(args, argv[0]);
104+
}
105+
} catch (const std::exception& e) {
106+
tfm::format(std::cerr, "Error: %s\nTry '%s --help' for more information.\n", e.what(), argv[0]);
107+
return EXIT_FAILURE;
108+
}
109+
return EXIT_SUCCESS;
110+
}
111+
112+
CommandLine ParseCommandLine(int argc, char* argv[])
113+
{
114+
CommandLine cmd;
115+
cmd.args.reserve(argc);
116+
for (int i = 1; i < argc; ++i) {
117+
std::string_view arg = argv[i];
118+
if (!cmd.command.empty()) {
119+
cmd.args.emplace_back(argv[i]);
120+
} else if (arg == "-m" || arg == "--multiprocess") {
121+
cmd.use_multiprocess = true;
122+
} else if (arg == "-M" || arg == "--monolithic") {
123+
cmd.use_multiprocess = false;
124+
} else if (arg == "-v" || arg == "--version") {
125+
cmd.show_version = true;
126+
} else if (arg == "-h" || arg == "--help" || arg == "help") {
127+
cmd.show_help = true;
128+
} else if (cmd.show_help && (arg == "-a" || arg == "--all")) {
129+
cmd.show_help_all = true;
130+
} else if (arg.starts_with("-")) {
131+
throw std::runtime_error(strprintf("Unknown option: %s", arg));
132+
} else if (!arg.empty()) {
133+
cmd.command = arg;
134+
}
135+
}
136+
return cmd;
137+
}
138+
139+
//! Execute the specified bitcoind, bitcoin-qt or other command line in `args`
140+
//! using src, bin and libexec directory paths relative to this executable, where
141+
//! the path to this executable is specified in `wrapper_argv0`.
142+
//!
143+
//! @param args Command line arguments to execute, where first argument should
144+
//! be a relative path to a bitcoind, bitcoin-qt or other executable
145+
//! that will be located on the PATH or relative to wrapper_argv0.
146+
//!
147+
//! @param wrapper_argv0 String containing first command line argument passed to
148+
//! main() to run the current executable. This is used to
149+
//! help determine the path to the current executable and
150+
//! how to look for new executables.
151+
//
152+
//! @note This function doesn't currently print anything but can be debugged
153+
//! from the command line using strace or dtrace like:
154+
//!
155+
//! strace -e trace=execve -s 10000 build/bin/bitcoin ...
156+
//! dtrace -n 'proc:::exec-success /pid == $target/ { trace(curpsinfo->pr_psargs); }' -c ...
157+
static void ExecCommand(const std::vector<const char*>& args, std::string_view wrapper_argv0)
158+
{
159+
// Construct argument string for execvp
160+
std::vector<const char*> exec_args{args};
161+
exec_args.emplace_back(nullptr);
162+
163+
// Try to call ExecVp with given exe path.
164+
auto try_exec = [&](fs::path exe_path, bool allow_notfound = true) {
165+
std::string exe_path_str{fs::PathToString(exe_path)};
166+
exec_args[0] = exe_path_str.c_str();
167+
if (util::ExecVp(exec_args[0], (char*const*)exec_args.data()) == -1) {
168+
if (allow_notfound && errno == ENOENT) return false;
169+
throw std::system_error(errno, std::system_category(), strprintf("execvp failed to execute '%s'", exec_args[0]));
170+
}
171+
throw std::runtime_error("execvp returned unexpectedly");
172+
};
173+
174+
// Get the wrapper executable path.
175+
const fs::path wrapper_path{util::GetExePath(wrapper_argv0)};
176+
177+
// Try to resolve any symlinks and figure out the directory containing the wrapper executable.
178+
std::error_code ec;
179+
fs::path wrapper_dir{fs::weakly_canonical(wrapper_path, ec)};
180+
if (wrapper_dir.empty()) wrapper_dir = wrapper_path; // Restore previous path if weakly_canonical failed.
181+
wrapper_dir = wrapper_dir.parent_path();
182+
183+
// Get path of the executable to be invoked.
184+
const fs::path arg0{fs::PathFromString(args[0])};
185+
186+
// Decide whether to fall back to the operating system to search for the
187+
// specified executable. Avoid doing this if it looks like the wrapper
188+
// executable was invoked by path, rather than by search, to avoid
189+
// unintentionally launching system executables in a local build.
190+
// (https://github.com/bitcoin/bitcoin/pull/31375#discussion_r1861814807)
191+
const bool fallback_os_search{!fs::PathFromString(std::string{wrapper_argv0}).has_parent_path()};
192+
193+
// If wrapper is installed in a bin/ directory, look for target executable
194+
// in libexec/
195+
(wrapper_dir.filename() == "bin" && try_exec(fs::path{wrapper_dir.parent_path()} / "libexec" / arg0.filename())) ||
196+
#ifdef WIN32
197+
// Otherwise check the "daemon" subdirectory in a windows install.
198+
(!wrapper_dir.empty() && try_exec(wrapper_dir / "daemon" / arg0.filename())) ||
199+
#endif
200+
// Otherwise look for target executable next to current wrapper
201+
(!wrapper_dir.empty() && try_exec(wrapper_dir / arg0.filename(), fallback_os_search)) ||
202+
// Otherwise just look on the system path.
203+
(fallback_os_search && try_exec(arg0.filename(), false));
204+
}

0 commit comments

Comments
 (0)