Merge bitcoin/bitcoin#31375: multiprocess: Add bitcoin wrapper executable

a5ac43d98d doc: Add release notes describing bitcoin wrapper executable (Ryan Ofsky)
258bda80c0 doc: Mention bitcoin wrapper executable in documentation (Ryan Ofsky)
d2739d75c9 build: add bitcoin.exe to windows installer (Sjors Provoost)
ba649c0006 ci: Run multiprocess tests through wrapper executable (Ryan Ofsky)
29bdd743bb test: Support BITCOIN_CMD environment variable (Ryan Ofsky)
9c8c68891b multiprocess: Add bitcoin wrapper executable (Ryan Ofsky)
5076d20fdb util: Add cross-platform ExecVp and GetExePath functions (Ryan Ofsky)

Pull request description:

  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 https://github.com/bitcoin/bitcoin/issues/30983.

  ---

  Initial implementation of this feature is deliberately minimal so the UX can evolve in response to feedback and there are not too many details to debate and discuss in a single PR. But many improvements are possible or planned:

  - Adding manpage and bash completions.
  - Showing nicer error messages that detect if an executable isn't installed and suggest how to fix [(comment)](https://github.com/bitcoin/bitcoin/pull/31375#discussion_r2073194474)
  - Showing wrapper command lines in subcommand in help output [(comment)](https://github.com/bitcoin/bitcoin/pull/31375#discussion_r2077800405). This could be done conditionally as suggested in the comment or be unconditional.
  - Showing wrapper command lines in subcommand error output. There is a bitcoin-cli error pointed out in [(comment)](https://github.com/bitcoin/bitcoin/pull/31375#discussion_r2091152243) that is needlessly confusing.
  - Integrating help so `bitcoin help subcommand` invokes `bitcoin subcommand -h`. `bitcoin -h subcommand` should also be supported and be equivalent [(comment)](https://github.com/bitcoin/bitcoin/pull/31375#discussion_r2093116725)
  - Adding support for `bitcoin-util` subcommands. Ideal interface would probably be more like `bitcoin grind` not `bitcoin util grind` but this has been punted for now. Supporting subcommands directly would require some ArgsManager modifications
  - Adding a dedicated python functional test for the wrapper. Right now there is some CI coverage by setting the `BITCOIN_CMD` variable, but this doesn't cover things like the help output and version output, and support for different directory layouts.
  - Better `--multiprocess` (`-m`) / `--monolithic` (`-M`) default selection. Right now, default is monolithic but it probably makes sense to chose more intelligently depending on whether -ipc options are enabled and what binaries are available.
  - Maybe parsing `bitcoin.conf` and supporting options to control wrapper behavior like custom locations or preferences or aliases.
  - Better command command line usability. Allow combining short options like (`-ah`). Allow fuzzy matching of subcommands or suggestions if you misspell. (suggested by stickies in review club)
  - Not directly related to this PR but `bitcoin-cli named` implementation used by the wrapper should do a better job disambiguating named arguments from base64 arguments ending in = as pointed out in [(comment)](https://github.com/bitcoin/bitcoin/pull/31375#discussion_r2091886628)

  ---

  This PR is part of the [process separation project](https://github.com/bitcoin/bitcoin/issues/28722). A review club meeting for it took place in https://bitcoincore.reviews/31375

ACKs for top commit:
  Sjors:
    utACK a5ac43d98d
  achow101:
    ACK a5ac43d98d
  vasild:
    ACK a5ac43d98d
  theStack:
    ACK a5ac43d98d
  ismaelsadeeq:
    fwiw my last review implied an ACK a5ac43d98d
  hodlinator:
    ACK a5ac43d98d

Tree-SHA512: 570e6a4ff8bd79ef6554da3d01f36c0a7c6d2dd7dace8f8732eca98f4a8bc2284474a9beadeba783114fe2f3dd08b2041b3da7753bae0b7f881ec50668cb821f
This commit is contained in:
Ava Chow
2025-05-27 12:38:19 -07:00
26 changed files with 392 additions and 21 deletions

View File

@ -94,6 +94,7 @@ endif()
#=============================
include(CMakeDependentOption)
# When adding a new option, end the <help_text> with a full stop for consistency.
option(BUILD_BITCOIN_BIN "Build bitcoin executable." ON)
option(BUILD_DAEMON "Build bitcoind executable." ON)
option(BUILD_GUI "Build bitcoin-qt executable." OFF)
option(BUILD_CLI "Build bitcoin-cli executable." ON)
@ -202,6 +203,7 @@ target_link_libraries(core_interface INTERFACE
if(BUILD_FOR_FUZZING)
message(WARNING "BUILD_FOR_FUZZING=ON will disable all other targets and force BUILD_FUZZ_BINARY=ON.")
set(BUILD_BITCOIN_BIN OFF)
set(BUILD_DAEMON OFF)
set(BUILD_CLI OFF)
set(BUILD_TX OFF)
@ -647,6 +649,7 @@ message("\n")
message("Configure summary")
message("=================")
message("Executables:")
message(" bitcoin ............................. ${BUILD_BITCOIN_BIN}")
message(" bitcoind ............................ ${BUILD_DAEMON}")
if(BUILD_DAEMON AND ENABLE_IPC)
set(bitcoin_daemon_status ON)

View File

@ -20,4 +20,4 @@ export BITCOIN_CONFIG="\
-DCMAKE_CXX_COMPILER='clang++;-m32' \
-DAPPEND_CPPFLAGS='-DBOOST_MULTI_INDEX_ENABLE_SAFE_MODE' \
"
export BITCOIND=bitcoin-node # Used in functional tests
export BITCOIN_CMD="bitcoin -m" # Used in functional tests

View File

@ -7,6 +7,7 @@ function(generate_setup_nsi)
set(abs_top_builddir ${PROJECT_BINARY_DIR})
set(CLIENT_URL ${PROJECT_HOMEPAGE_URL})
set(CLIENT_TARNAME "bitcoin")
set(BITCOIN_WRAPPER_NAME "bitcoin")
set(BITCOIN_GUI_NAME "bitcoin-qt")
set(BITCOIN_DAEMON_NAME "bitcoind")
set(BITCOIN_CLI_NAME "bitcoin-cli")

View File

@ -23,7 +23,7 @@ function(add_maintenance_targets)
return()
endif()
foreach(target IN ITEMS bitcoind bitcoin-qt bitcoin-cli bitcoin-tx bitcoin-util bitcoin-wallet test_bitcoin bench_bitcoin)
foreach(target IN ITEMS bitcoin bitcoind bitcoin-qt bitcoin-cli bitcoin-tx bitcoin-util bitcoin-wallet test_bitcoin bench_bitcoin)
if(TARGET ${target})
list(APPEND executables $<TARGET_FILE:${target}>)
endif()
@ -43,7 +43,7 @@ function(add_maintenance_targets)
endfunction()
function(add_windows_deploy_target)
if(MINGW AND TARGET bitcoin-qt AND TARGET bitcoind AND TARGET bitcoin-cli AND TARGET bitcoin-tx AND TARGET bitcoin-wallet AND TARGET bitcoin-util AND TARGET test_bitcoin)
if(MINGW AND TARGET bitcoin AND TARGET bitcoin-qt AND TARGET bitcoind AND TARGET bitcoin-cli AND TARGET bitcoin-tx AND TARGET bitcoin-wallet AND TARGET bitcoin-util AND TARGET test_bitcoin)
find_program(MAKENSIS_EXECUTABLE makensis)
if(NOT MAKENSIS_EXECUTABLE)
add_custom_target(deploy
@ -59,6 +59,7 @@ function(add_windows_deploy_target)
add_custom_command(
OUTPUT ${PROJECT_BINARY_DIR}/bitcoin-win64-setup.exe
COMMAND ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/release
COMMAND ${CMAKE_STRIP} $<TARGET_FILE:bitcoin> -o ${PROJECT_BINARY_DIR}/release/$<TARGET_FILE_NAME:bitcoin>
COMMAND ${CMAKE_STRIP} $<TARGET_FILE:bitcoin-qt> -o ${PROJECT_BINARY_DIR}/release/$<TARGET_FILE_NAME:bitcoin-qt>
COMMAND ${CMAKE_STRIP} $<TARGET_FILE:bitcoind> -o ${PROJECT_BINARY_DIR}/release/$<TARGET_FILE_NAME:bitcoind>
COMMAND ${CMAKE_STRIP} $<TARGET_FILE:bitcoin-cli> -o ${PROJECT_BINARY_DIR}/release/$<TARGET_FILE_NAME:bitcoin-cli>

View File

@ -126,6 +126,9 @@ def check_ELF_FORTIFY(binary) -> bool:
# bitcoin-util does not currently contain any fortified functions
if 'Bitcoin Core bitcoin-util utility version ' in binary.strings:
return True
# bitcoin wrapper does not currently contain any fortified functions
if '--monolithic' in binary.strings:
return True
chk_funcs = set()

View File

@ -62,6 +62,8 @@ bitcoin-cli -named createwallet wallet_name=mywallet load_on_startup=true
bitcoin-cli -named createwallet mywallet load_on_startup=true
```
`bitcoin rpc` can also be substituted for `bitcoin-cli -named`, and is a newer alternative.
## Versioning
The RPC interface might change from one major version of Bitcoin Core to the

View File

@ -17,6 +17,9 @@ Unpack the files into a directory and run:
- `bin/bitcoin-qt` (GUI) or
- `bin/bitcoind` (headless)
- `bin/bitcoin` (wrapper command)
The `bitcoin` command supports subcommands like `bitcoin gui`, `bitcoin node`, and `bitcoin rpc` exposing different functionality. Subcommands can be listed with `bitcoin help`.
### Windows

View File

@ -199,6 +199,10 @@ cmake --build build --target deploy
Bitcoin Core should now be available at `./build/bin/bitcoind`.
If you compiled support for the GUI, it should be available at `./build/bin/bitcoin-qt`.
There is also a multifunction command line interface at `./build/bin/bitcoin`
supporting subcommands like `bitcoin node`, `bitcoin gui`, `bitcoin rpc`, and
others that can be listed with `bitcoin help`.
The first time you run `bitcoind` or `bitcoin-qt`, it will start downloading the blockchain.
This process could take many hours, or even days on slower than average systems.

View File

@ -174,4 +174,5 @@ This example lists the steps necessary to setup and build a command line only di
cmake --build build
ctest --test-dir build
./build/bin/bitcoind
./build/bin/bitcoin help

View File

@ -113,3 +113,5 @@ To see which CJDNS peers your node is connected to, use `bitcoin-cli -netinfo 4`
or the `getpeerinfo` RPC (i.e. `bitcoin-cli getpeerinfo`).
You can use the `getnodeaddresses` RPC to fetch a number of CJDNS peers known to your node; run `bitcoin-cli help getnodeaddresses` for details.
`bitcoin rpc` can also be substituted for `bitcoin-cli`.

View File

@ -14,6 +14,8 @@ Start Bitcoin Core:
$ bitcoind -signer=../HWI/hwi.py
```
`bitcoin node` can also be substituted for `bitcoind`.
### Device setup
Follow the hardware manufacturers instructions for the initial device setup, as well as their instructions for creating a backup. Alternatively, for some devices, you can use the `setup`, `restore` and `backup` commands provided by [HWI](https://github.com/bitcoin-core/HWI).
@ -40,6 +42,8 @@ Create a wallet, this automatically imports the public keys:
$ bitcoin-cli createwallet "hww" true true "" true true true
```
`bitcoin rpc` can also be substituted for `bitcoin-cli`.
### Verify an address
Display an address on the device:

View File

@ -15,6 +15,8 @@ The following command, for example, creates a descriptor wallet. More informatio
$ bitcoin-cli createwallet "wallet-01"
```
`bitcoin rpc` can also be substituted for `bitcoin-cli`.
By default, wallets are created in the `wallets` folder of the data directory, which varies by operating system, as shown below. The user can change the default by using the `-datadir` or `-walletdir` initialization parameters.
| Operating System | Default wallet directory |

View File

@ -25,8 +25,8 @@ make -C depends NO_QT=1 MULTIPROCESS=1
HOST_PLATFORM="x86_64-pc-linux-gnu"
cmake -B build --toolchain=depends/$HOST_PLATFORM/toolchain.cmake
cmake --build build
build/bin/bitcoin-node -regtest -printtoconsole -debug=ipc
BITCOIND=$(pwd)/build/bin/bitcoin-node build/test/functional/test_runner.py
build/bin/bitcoin -m node -regtest -printtoconsole -debug=ipc
BITCOIN_CMD="bitcoin -m" build/test/functional/test_runner.py
```
The `cmake` build will pick up settings and library locations from the depends directory, so there is no need to pass `-DENABLE_IPC=ON` as a separate flag when using the depends system (it's controlled by the `MULTIPROCESS=1` option).
@ -41,6 +41,11 @@ By default when `-DENABLE_IPC=ON` is enabled, the libmultiprocess sources at [..
## Usage
`bitcoin-node` is a drop-in replacement for `bitcoind`, and `bitcoin-gui` is a drop-in replacement for `bitcoin-qt`, and there are no differences in use or external behavior between the new and old executables. But internally after [#10102](https://github.com/bitcoin/bitcoin/pull/10102), `bitcoin-gui` will spawn a `bitcoin-node` process to run P2P and RPC code, communicating with it across a socket pair, and `bitcoin-node` will spawn `bitcoin-wallet` to run wallet code, also communicating over a socket pair. This will let node, wallet, and GUI code run in separate address spaces for better isolation, and allow future improvements like being able to start and stop components independently on different machines and environments.
[#19460](https://github.com/bitcoin/bitcoin/pull/19460) also adds a new `bitcoin-node` `-ipcbind` option and a `bitcoind-wallet` `-ipcconnect` option to allow new wallet processes to connect to an existing node process.
And [#19461](https://github.com/bitcoin/bitcoin/pull/19461) adds a new `bitcoin-gui` `-ipcconnect` option to allow new GUI processes to connect to an existing node process.
Recommended way to use multiprocess binaries is to invoke `bitcoin` CLI like `bitcoin -m node -debug=ipc` or `bitcoin -m gui -printtoconsole -debug=ipc`.
When the `-m` (`--multiprocess`) option is used the `bitcoin` command will execute multiprocess binaries instead of monolithic ones (`bitcoin-node` instead of `bitcoind`, and `bitcoin-gui` instead of `bitcoin-qt`). The multiprocess binaries can also be invoked directly, but this is not recommended as they may change or be renamed in the future, and they are not installed in the PATH.
The multiprocess binaries currently function the same as the monolithic binaries, except they support an `-ipcbind` option.
In the future, after [#10102](https://github.com/bitcoin/bitcoin/pull/10102) they will have other differences. Specifically `bitcoin-gui` will spawn a `bitcoin-node` process to run P2P and RPC code, communicating with it across a socket pair, and `bitcoin-node` will spawn `bitcoin-wallet` to run wallet code, also communicating over a socket pair. This will let node, wallet, and GUI code run in separate address spaces for better isolation, and allow future improvements like being able to start and stop components independently on different machines and environments. [#19460](https://github.com/bitcoin/bitcoin/pull/19460) also adds a new `bitcoin-wallet -ipcconnect` option to allow new wallet processes to connect to an existing node process.
And [#19461](https://github.com/bitcoin/bitcoin/pull/19461) adds a new `bitcoin-gui -ipcconnect` option to allow new GUI processes to connect to an existing node process.

View File

@ -31,6 +31,8 @@ do
done
```
`bitcoin rpc` can also be substituted for `bitcoin-cli`.
Extract the xpub of each wallet. To do this, the `listdescriptors` RPC is used. By default, Bitcoin Core single-sig wallets are created using path `m/44'/1'/0'` for PKH, `m/84'/1'/0'` for WPKH, `m/49'/1'/0'` for P2WPKH-nested-in-P2SH and `m/86'/1'/0'` for P2TR based accounts. Each of them uses the chain 0 for external addresses and chain 1 for internal ones, as shown in the example below.
```

View File

@ -34,6 +34,8 @@ We are going to first create an `offline_wallet` on the offline host. We will th
}
```
`bitcoin rpc` can also be substituted for `bitcoin-cli`.
> [!NOTE]
> The use of a passphrase is crucial to encrypt the wallet.dat file. This encryption ensures that even if an unauthorized individual gains access to the offline host, they won't be able to access the wallet's contents. Further details about securing your wallet can be found in [Managing the Wallet](https://github.com/bitcoin/bitcoin/blob/master/doc/managing-wallets.md#12-encrypting-the-wallet)

View File

@ -0,0 +1,11 @@
New command line interface
--------------------------
A new `bitcoin` command line tool has been added to make features more
discoverable and convenient to use. The `bitcoin` tool just calls other
executables and does not implement any functionality on its own. Specifically
`bitcoin node` is a synonym for `bitcoind`, `bitcoin gui` is a synonym for
`bitcoin-qt`, and `bitcoin rpc` is a synonym for `bitcoin-cli -named`. Other
commands and options can be listed with `bitcoin help`. The new tool does not
replace other tools, so all existing commands should continue working and there
are no plans to deprecate them.

View File

@ -27,6 +27,8 @@ e.g. for `-onlynet=onion`.
You can use the `getnodeaddresses` RPC to fetch a number of onion peers known to your node; run `bitcoin-cli help getnodeaddresses` for details.
`bitcoin rpc` can also be substituted for `bitcoin-cli`.
## 1. Run Bitcoin Core behind a Tor proxy
The first step is running Bitcoin Core behind a Tor proxy. This will already anonymize all
@ -64,6 +66,8 @@ In a typical situation, this suffices to run behind a Tor proxy:
./bitcoind -proxy=127.0.0.1:9050
`bitcoin node` or `bitcoin gui` can also be substituted for `bitcoind`.
## 2. Automatically create a Bitcoin Core onion service
Bitcoin Core makes use of Tor's control socket API to create and destroy

View File

@ -87,6 +87,8 @@ For instance:
-zmqpubrawtx=ipc:///tmp/bitcoind.tx.raw \
-zmqpubhashtxhwm=10000
`bitcoin node` or `bitcoin gui` can also be substituted for `bitcoind`.
Notification types correspond to message topics (details in next section). For instance,
for the notification `-zmqpubhashtx` the topic is `hashtx`. These options can also be
provided in bitcoin.conf.

View File

@ -73,6 +73,7 @@ Section -Main SEC0000
SetOutPath $INSTDIR
SetOverwrite on
File @abs_top_builddir@/release/@BITCOIN_GUI_NAME@@EXEEXT@
File @abs_top_builddir@/release/@BITCOIN_WRAPPER_NAME@@EXEEXT@
File /oname=COPYING.txt @abs_top_srcdir@/COPYING
File /oname=readme.txt @abs_top_srcdir@/doc/README_windows.txt
File @abs_top_srcdir@/share/examples/bitcoin.conf
@ -129,6 +130,7 @@ done${UNSECTION_ID}:
# Uninstaller sections
Section /o -un.Main UNSEC0000
Delete /REBOOTOK $INSTDIR\@BITCOIN_WRAPPER_NAME@@EXEEXT@
Delete /REBOOTOK $INSTDIR\@BITCOIN_GUI_NAME@@EXEEXT@
Delete /REBOOTOK $INSTDIR\COPYING.txt
Delete /REBOOTOK $INSTDIR\readme.txt

View File

@ -332,6 +332,12 @@ target_link_libraries(bitcoin_node
$<TARGET_NAME_IF_EXISTS:USDT::headers>
)
# Bitcoin wrapper executable that can call other executables.
if(BUILD_BITCOIN_BIN)
add_executable(bitcoin bitcoin.cpp)
target_link_libraries(bitcoin core_interface bitcoin_util)
install_binary_component(bitcoin)
endif()
# Bitcoin Core bitcoind.
if(BUILD_DAEMON)

204
src/bitcoin.cpp Normal file
View File

@ -0,0 +1,204 @@
// Copyright (c) 2025 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <bitcoin-build-config.h> // IWYU pragma: keep
#include <clientversion.h>
#include <util/fs.h>
#include <util/exec.h>
#include <util/strencodings.h>
#include <util/translation.h>
#include <iostream>
#include <string>
#include <tinyformat.h>
#include <vector>
const TranslateFn G_TRANSLATION_FUN{nullptr};
static constexpr auto HELP_USAGE = R"(Usage: %s [OPTIONS] COMMAND...
Options:
-m, --multiprocess Run multiprocess binaries bitcoin-node, bitcoin-gui.
-M, --monolithic Run monolithic binaries bitcoind, bitcoin-qt. (Default behavior)
-v, --version Show version information
-h, --help Show this help message
Commands:
gui [ARGS] Start GUI, equivalent to running 'bitcoin-qt [ARGS]' or 'bitcoin-gui [ARGS]'.
node [ARGS] Start node, equivalent to running 'bitcoind [ARGS]' or 'bitcoin-node [ARGS]'.
rpc [ARGS] Call RPC method, equivalent to running 'bitcoin-cli -named [ARGS]'.
wallet [ARGS] Call wallet command, equivalent to running 'bitcoin-wallet [ARGS]'.
tx [ARGS] Manipulate hex-encoded transactions, equivalent to running 'bitcoin-tx [ARGS]'.
help [-a] Show this help message. Include -a or --all to show additional commands.
)";
static constexpr auto HELP_EXTRA = R"(
Additional less commonly used commands:
bench [ARGS] Run bench command, equivalent to running 'bench_bitcoin [ARGS]'.
chainstate [ARGS] Run bitcoin kernel chainstate util, equivalent to running 'bitcoin-chainstate [ARGS]'.
test [ARGS] Run unit tests, equivalent to running 'test_bitcoin [ARGS]'.
test-gui [ARGS] Run GUI unit tests, equivalent to running 'test_bitcoin-qt [ARGS]'.
)";
struct CommandLine {
bool use_multiprocess{false};
bool show_version{false};
bool show_help{false};
bool show_help_all{false};
std::string_view command;
std::vector<const char*> args;
};
CommandLine ParseCommandLine(int argc, char* argv[]);
static void ExecCommand(const std::vector<const char*>& args, std::string_view argv0);
int main(int argc, char* argv[])
{
try {
CommandLine cmd{ParseCommandLine(argc, argv)};
if (cmd.show_version) {
tfm::format(std::cout, "%s version %s\n%s", CLIENT_NAME, FormatFullVersion(), FormatParagraph(LicenseInfo()));
return EXIT_SUCCESS;
}
std::vector<const char*> args;
if (cmd.show_help || cmd.command.empty()) {
tfm::format(std::cout, HELP_USAGE, argv[0]);
if (cmd.show_help_all) tfm::format(std::cout, HELP_EXTRA);
return cmd.show_help ? EXIT_SUCCESS : EXIT_FAILURE;
} else if (cmd.command == "gui") {
args.emplace_back(cmd.use_multiprocess ? "bitcoin-gui" : "bitcoin-qt");
} else if (cmd.command == "node") {
args.emplace_back(cmd.use_multiprocess ? "bitcoin-node" : "bitcoind");
} else if (cmd.command == "rpc") {
args.emplace_back("bitcoin-cli");
// Since "bitcoin rpc" is a new interface that doesn't need to be
// backward compatible, enable -named by default so it is convenient
// for callers to use a mix of named and unnamed parameters. Callers
// can override this by specifying -nonamed, but should not need to
// unless they are passing string values containing '=' characters
// as unnamed parameters.
args.emplace_back("-named");
} else if (cmd.command == "wallet") {
args.emplace_back("bitcoin-wallet");
} else if (cmd.command == "tx") {
args.emplace_back("bitcoin-tx");
} else if (cmd.command == "bench") {
args.emplace_back("bench_bitcoin");
} else if (cmd.command == "chainstate") {
args.emplace_back("bitcoin-chainstate");
} else if (cmd.command == "test") {
args.emplace_back("test_bitcoin");
} else if (cmd.command == "test-gui") {
args.emplace_back("test_bitcoin-qt");
} else if (cmd.command == "util") {
args.emplace_back("bitcoin-util");
} else {
throw std::runtime_error(strprintf("Unrecognized command: '%s'", cmd.command));
}
if (!args.empty()) {
args.insert(args.end(), cmd.args.begin(), cmd.args.end());
ExecCommand(args, argv[0]);
}
} catch (const std::exception& e) {
tfm::format(std::cerr, "Error: %s\nTry '%s --help' for more information.\n", e.what(), argv[0]);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
CommandLine ParseCommandLine(int argc, char* argv[])
{
CommandLine cmd;
cmd.args.reserve(argc);
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (!cmd.command.empty()) {
cmd.args.emplace_back(argv[i]);
} else if (arg == "-m" || arg == "--multiprocess") {
cmd.use_multiprocess = true;
} else if (arg == "-M" || arg == "--monolithic") {
cmd.use_multiprocess = false;
} else if (arg == "-v" || arg == "--version") {
cmd.show_version = true;
} else if (arg == "-h" || arg == "--help" || arg == "help") {
cmd.show_help = true;
} else if (cmd.show_help && (arg == "-a" || arg == "--all")) {
cmd.show_help_all = true;
} else if (arg.starts_with("-")) {
throw std::runtime_error(strprintf("Unknown option: %s", arg));
} else if (!arg.empty()) {
cmd.command = arg;
}
}
return cmd;
}
//! Execute the specified bitcoind, bitcoin-qt or other command line in `args`
//! using src, bin and libexec directory paths relative to this executable, where
//! the path to this executable is specified in `wrapper_argv0`.
//!
//! @param args Command line arguments to execute, where first argument should
//! be a relative path to a bitcoind, bitcoin-qt or other executable
//! that will be located on the PATH or relative to wrapper_argv0.
//!
//! @param wrapper_argv0 String containing first command line argument passed to
//! main() to run the current executable. This is used to
//! help determine the path to the current executable and
//! how to look for new executables.
//
//! @note This function doesn't currently print anything but can be debugged
//! from the command line using strace or dtrace like:
//!
//! strace -e trace=execve -s 10000 build/bin/bitcoin ...
//! dtrace -n 'proc:::exec-success /pid == $target/ { trace(curpsinfo->pr_psargs); }' -c ...
static void ExecCommand(const std::vector<const char*>& args, std::string_view wrapper_argv0)
{
// Construct argument string for execvp
std::vector<const char*> exec_args{args};
exec_args.emplace_back(nullptr);
// Try to call ExecVp with given exe path.
auto try_exec = [&](fs::path exe_path, bool allow_notfound = true) {
std::string exe_path_str{fs::PathToString(exe_path)};
exec_args[0] = exe_path_str.c_str();
if (util::ExecVp(exec_args[0], (char*const*)exec_args.data()) == -1) {
if (allow_notfound && errno == ENOENT) return false;
throw std::system_error(errno, std::system_category(), strprintf("execvp failed to execute '%s'", exec_args[0]));
}
throw std::runtime_error("execvp returned unexpectedly");
};
// Get the wrapper executable path.
const fs::path wrapper_path{util::GetExePath(wrapper_argv0)};
// Try to resolve any symlinks and figure out the directory containing the wrapper executable.
std::error_code ec;
fs::path wrapper_dir{fs::weakly_canonical(wrapper_path, ec)};
if (wrapper_dir.empty()) wrapper_dir = wrapper_path; // Restore previous path if weakly_canonical failed.
wrapper_dir = wrapper_dir.parent_path();
// Get path of the executable to be invoked.
const fs::path arg0{fs::PathFromString(args[0])};
// Decide whether to fall back to the operating system to search for the
// specified executable. Avoid doing this if it looks like the wrapper
// executable was invoked by path, rather than by search, to avoid
// unintentionally launching system executables in a local build.
// (https://github.com/bitcoin/bitcoin/pull/31375#discussion_r1861814807)
const bool fallback_os_search{!fs::PathFromString(std::string{wrapper_argv0}).has_parent_path()};
// If wrapper is installed in a bin/ directory, look for target executable
// in libexec/
(wrapper_dir.filename() == "bin" && try_exec(fs::path{wrapper_dir.parent_path()} / "libexec" / arg0.filename())) ||
#ifdef WIN32
// Otherwise check the "daemon" subdirectory in a windows install.
(!wrapper_dir.empty() && try_exec(wrapper_dir / "daemon" / arg0.filename())) ||
#endif
// Otherwise look for target executable next to current wrapper
(!wrapper_dir.empty() && try_exec(wrapper_dir / arg0.filename(), fallback_os_search)) ||
// Otherwise just look on the system path.
(fallback_os_search && try_exec(arg0.filename(), false));
}

View File

@ -9,6 +9,7 @@ add_library(bitcoin_util STATIC EXCLUDE_FROM_ALL
bytevectorhash.cpp
chaintype.cpp
check.cpp
exec.cpp
exception.cpp
feefrac.cpp
fs.cpp

75
src/util/exec.cpp Normal file
View File

@ -0,0 +1,75 @@
// Copyright (c) 2025 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <util/exec.h>
#include <util/fs.h>
#include <util/subprocess.h>
#include <string>
#include <vector>
#ifdef WIN32
#include <process.h>
#include <windows.h>
#else
#include <unistd.h>
#endif
namespace util {
int ExecVp(const char* file, char* const argv[])
{
#ifndef WIN32
return execvp(file, argv);
#else
std::vector<std::wstring> escaped_args;
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
for (char* const* arg_ptr{argv}; *arg_ptr; ++arg_ptr) {
subprocess::util::quote_argument(converter.from_bytes(*arg_ptr), escaped_args.emplace_back(), false);
}
std::vector<const wchar_t*> new_argv;
new_argv.reserve(escaped_args.size() + 1);
for (const auto& s : escaped_args) new_argv.push_back(s.c_str());
new_argv.push_back(nullptr);
return _wexecvp(converter.from_bytes(file).c_str(), new_argv.data());
#endif
}
fs::path GetExePath(std::string_view argv0)
{
// Try to figure out where executable is located. This does a simplified
// search that won't work perfectly on every platform and doesn't need to,
// as it is only currently being used in a convenience wrapper binary to try
// to prioritize locally built or installed executables over system
// executables.
const fs::path argv0_path{fs::PathFromString(std::string{argv0})};
fs::path path{argv0_path};
std::error_code ec;
#ifndef WIN32
// If argv0 doesn't contain a path separator, it was invoked from the system
// PATH and can be searched for there.
if (!argv0_path.has_parent_path()) {
if (const char* path_env = std::getenv("PATH")) {
size_t start{0}, end{0};
for (std::string_view paths{path_env}; end != std::string_view::npos; start = end + 1) {
end = paths.find(':', start);
fs::path candidate = fs::path(paths.substr(start, end - start)) / argv0_path;
if (fs::is_regular_file(candidate, ec)) {
path = candidate;
break;
}
}
}
}
#else
wchar_t module_path[MAX_PATH];
if (GetModuleFileNameW(nullptr, module_path, MAX_PATH) > 0) {
path = fs::path{module_path};
}
#endif
return path;
}
} // namespace util

23
src/util/exec.h Normal file
View File

@ -0,0 +1,23 @@
// Copyright (c) 2025 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_UTIL_EXEC_H
#define BITCOIN_UTIL_EXEC_H
#include <util/fs.h>
#include <string_view>
namespace util {
//! Cross-platform wrapper for POSIX execvp function.
//! Arguments and return value are the same as for POSIX execvp, and the argv
//! array should consist of null terminated strings and be null terminated
//! itself, like the POSIX function.
int ExecVp(const char* file, char* const argv[]);
//! Return path to current executable assuming it was invoked with argv0.
//! If path could not be determined, returns an empty path.
fs::path GetExePath(std::string_view argv0);
} // namespace util
#endif // BITCOIN_UTIL_EXEC_H

View File

@ -13,6 +13,7 @@ import platform
import pdb
import random
import re
import shlex
import shutil
import subprocess
import sys
@ -73,33 +74,37 @@ class Binaries:
self.paths = paths
self.bin_dir = bin_dir
def daemon_argv(self):
def node_argv(self):
"Return argv array that should be used to invoke bitcoind"
return self._argv(self.paths.bitcoind)
return self._argv("node", self.paths.bitcoind)
def rpc_argv(self):
"Return argv array that should be used to invoke bitcoin-cli"
return self._argv(self.paths.bitcoincli)
# Add -nonamed because "bitcoin rpc" enables -named by default, but bitcoin-cli doesn't
return self._argv("rpc", self.paths.bitcoincli) + ["-nonamed"]
def util_argv(self):
"Return argv array that should be used to invoke bitcoin-util"
return self._argv(self.paths.bitcoinutil)
return self._argv("util", self.paths.bitcoinutil)
def wallet_argv(self):
"Return argv array that should be used to invoke bitcoin-wallet"
return self._argv(self.paths.bitcoinwallet)
return self._argv("wallet", self.paths.bitcoinwallet)
def chainstate_argv(self):
"Return argv array that should be used to invoke bitcoin-chainstate"
return self._argv(self.paths.bitcoinchainstate)
return self._argv("chainstate", self.paths.bitcoinchainstate)
def _argv(self, bin_path):
"""Return argv array that should be used to invoke the command.
Normally this will return binary paths directly from the paths object,
but when bin_dir is set (by tests calling binaries from previous
releases) it will return paths relative to bin_dir instead."""
def _argv(self, command, bin_path):
"""Return argv array that should be used to invoke the command. It
either uses the bitcoin wrapper executable (if BITCOIN_CMD is set), or
the direct binary path (bitcoind, etc). When bin_dir is set (by tests
calling binaries from previous releases) it always uses the direct
path."""
if self.bin_dir is not None:
return [os.path.join(self.bin_dir, os.path.basename(bin_path))]
elif self.paths.bitcoin_cmd is not None:
return self.paths.bitcoin_cmd + [command]
else:
return [bin_path]
@ -293,6 +298,9 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
binary + self.config["environment"]["EXEEXT"],
)
setattr(paths, attribute_name, os.getenv(env_variable_name, default=default_filename))
# BITCOIN_CMD environment variable can be specified to invoke bitcoin
# wrapper binary instead of other executables.
paths.bitcoin_cmd = shlex.split(os.getenv("BITCOIN_CMD", "")) or None
return paths
def get_binaries(self, bin_dir=None):
@ -538,7 +546,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
bins_missing = False
for bin_path in (argv[0] for bin_dir in bin_dirs
for binaries in (self.get_binaries(bin_dir),)
for argv in (binaries.daemon_argv(), binaries.rpc_argv())):
for argv in (binaries.node_argv(), binaries.rpc_argv())):
if shutil.which(bin_path) is None:
self.log.error(f"Binary not found: {bin_path}")
bins_missing = True

View File

@ -107,7 +107,7 @@ class TestNode():
# Configuration for logging is set as command-line args rather than in the bitcoin.conf file.
# This means that starting a bitcoind using the temp dir to debug a failed test won't
# spam debug.log.
self.args = self.binaries.daemon_argv() + [
self.args = self.binaries.node_argv() + [
f"-datadir={self.datadir_path}",
"-logtimemicros",
"-debug",