| #include "../options.hpp" | |||||
| #include <dds/util/env.hpp> | |||||
| #include <dds/util/fs.hpp> | |||||
| #include <dds/util/paths.hpp> | |||||
| #include <dds/util/result.hpp> | |||||
| #include <dds/util/string.hpp> | |||||
| #include <boost/leaf/handle_exception.hpp> | |||||
| #include <fansi/styled.hpp> | |||||
| #include <fmt/ostream.h> | |||||
| #include <neo/assert.hpp> | |||||
| #include <neo/platform.hpp> | |||||
| #include <neo/scope.hpp> | |||||
| #ifdef __APPLE__ | |||||
| #include <mach-o/dyld.h> | |||||
| #elif __FreeBSD__ | |||||
| #include <sys/sysctl.h> | |||||
| #elif _WIN32 | |||||
| #include <windows.h> | |||||
| // Must be included second: | |||||
| #include <wil/resource.h> | |||||
| #endif | |||||
| using namespace fansi::literals; | |||||
| namespace dds::cli::cmd { | |||||
| namespace { | |||||
| fs::path current_executable() { | |||||
| #if __linux__ | |||||
| return fs::read_symlink("/proc/self/exe"); | |||||
| #elif __APPLE__ | |||||
| std::uint32_t len = 0; | |||||
| _NSGetExecutablePath(nullptr, &len); | |||||
| std::string buffer; | |||||
| buffer.resize(len + 1); | |||||
| auto rc = _NSGetExecutablePath(buffer.data(), &len); | |||||
| neo_assert(invariant, rc == 0, "Unexpected error from _NSGetExecutablePath()"); | |||||
| return fs::canonical(buffer); | |||||
| #elif __FreeBSD__ | |||||
| std::string buffer; | |||||
| int mib[] = {CTRL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1}; | |||||
| std::size_t len = 0; | |||||
| auto rc = ::sysctl(mib, 4, nullptr, &len, nullptr, 0); | |||||
| neo_assert(invariant, | |||||
| rc == 0, | |||||
| "Unexpected error from ::sysctl() while getting executable path", | |||||
| errno); | |||||
| buffer.resize(len + 1); | |||||
| rc = ::sysctl(mib, 4, buffer.data(), &len, nullptr, 0); | |||||
| neo_assert(invariant, | |||||
| rc == 0, | |||||
| "Unexpected error from ::sysctl() while getting executable path", | |||||
| errno); | |||||
| return fs::canonical(nullptr); | |||||
| #elif _WIN32 | |||||
| std::wstring buffer; | |||||
| while (true) { | |||||
| buffer.resize(buffer.size() + 32); | |||||
| auto reallen | |||||
| = ::GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size())); | |||||
| if (reallen == buffer.size() && ::GetLastError() == ERROR_INSUFFICIENT_BUFFER) { | |||||
| continue; | |||||
| } | |||||
| buffer.resize(reallen); | |||||
| return fs::canonical(buffer); | |||||
| } | |||||
| #else | |||||
| #error "No method of getting the current executable path is implemented on this system. FIXME!" | |||||
| #endif | |||||
| } | |||||
| fs::path user_binaries_dir() noexcept { | |||||
| #if _WIN32 | |||||
| return dds::user_data_dir() / "bin"; | |||||
| #else | |||||
| return dds::user_home_dir() / ".local/bin"; | |||||
| #endif | |||||
| } | |||||
| fs::path system_binaries_dir() noexcept { | |||||
| #if _WIN32 | |||||
| return "C:/bin"; | |||||
| #else | |||||
| return "/usr/local/bin"; | |||||
| #endif | |||||
| } | |||||
| void fixup_system_path(const options&) { | |||||
| #if !_WIN32 | |||||
| // We install into /usr/local/bin, and every nix-like system we support already has that on the | |||||
| // global PATH | |||||
| #else // Windows! | |||||
| #error "Not yet implemented" | |||||
| #endif | |||||
| } | |||||
| void fixup_user_path(const options& opts) { | |||||
| #if !_WIN32 | |||||
| auto profile_file = dds::user_home_dir() / ".profile"; | |||||
| auto profile_content = dds::slurp_file(profile_file); | |||||
| if (dds::contains(profile_content, "$HOME/.local/bin")) { | |||||
| // We'll assume that this is properly loading .local/bin for .profile | |||||
| dds_log(info, ".br.cyan[{}] is okay"_styled, profile_file); | |||||
| } else { | |||||
| // Let's add it | |||||
| profile_content | |||||
| += ("\n# This entry was added by 'dds install-yourself' for the user-local " | |||||
| "binaries\nPATH=$HOME/bin:$HOME/.local/bin:$PATH\n"); | |||||
| if (opts.dry_run) { | |||||
| dds_log(info, | |||||
| "Would update .br.cyan[{}] to have ~/.local/bin on $PATH"_styled, | |||||
| profile_file); | |||||
| } else { | |||||
| dds_log(info, | |||||
| "Updating .br.cyan[{}] with a user-local binaries PATH entry"_styled, | |||||
| profile_file); | |||||
| auto tmp_file = profile_file; | |||||
| tmp_file += ".tmp"; | |||||
| auto bak_file = profile_file; | |||||
| bak_file += ".bak"; | |||||
| // Move .profile back into place if we abore for any reason | |||||
| neo_defer { | |||||
| if (!fs::exists(profile_file)) { | |||||
| safe_rename(bak_file, profile_file); | |||||
| } | |||||
| }; | |||||
| // Write the temporary version | |||||
| dds::write_file(tmp_file, profile_content).value(); | |||||
| // Make a backup | |||||
| safe_rename(profile_file, bak_file); | |||||
| // Move the tmp over the final location | |||||
| safe_rename(tmp_file, profile_file); | |||||
| // Okay! | |||||
| dds_log(info, | |||||
| ".br.green[{}] was updated. Prior contents are safe in .br.cyan[{}]"_styled, | |||||
| profile_file, | |||||
| bak_file); | |||||
| } | |||||
| } | |||||
| auto fish_config = dds::user_config_dir() / "fish/config.fish"; | |||||
| if (fs::exists(fish_config)) { | |||||
| auto fish_config_content = slurp_file(fish_config); | |||||
| if (dds::contains(fish_config_content, "$HOME/.local/bin")) { | |||||
| // Assume that this is up-to-date | |||||
| dds_log(info, "Fish configuration in .br.cyan[{}] is okay"_styled, fish_config); | |||||
| } else { | |||||
| dds_log( | |||||
| info, | |||||
| "Updating Fish shell configuration .br.cyan[{}] with user-local binaries PATH entry"_styled, | |||||
| fish_config); | |||||
| fish_config_content | |||||
| += ("\n# This line was added by 'dds install-yourself' to add the usre-local " | |||||
| "binaries directory to $PATH\nset -x PATH $PATH \"$HOME/.local/bin\"\n"); | |||||
| auto tmp_file = fish_config; | |||||
| auto bak_file = fish_config; | |||||
| tmp_file += ".tmp"; | |||||
| bak_file += ".bak"; | |||||
| neo_defer { | |||||
| if (!fs::exists(fish_config)) { | |||||
| safe_rename(bak_file, fish_config); | |||||
| } | |||||
| }; | |||||
| // Write the temporary version | |||||
| dds::write_file(tmp_file, fish_config_content).value(); | |||||
| // Make a backup | |||||
| safe_rename(fish_config, bak_file); | |||||
| // Move the temp over the destination | |||||
| safe_rename(tmp_file, fish_config); | |||||
| // Okay! | |||||
| dds_log(info, | |||||
| ".br.green[{}] was updated. Prior contents are safe in .br.cyan[{}]"_styled, | |||||
| fish_config, | |||||
| bak_file); | |||||
| } | |||||
| } | |||||
| #else // _WIN32 | |||||
| #endif | |||||
| } | |||||
| void fixup_path(const options& opts) { | |||||
| if (opts.install_yourself.where == opts.install_yourself.system) { | |||||
| fixup_system_path(opts); | |||||
| } else { | |||||
| fixup_user_path(opts); | |||||
| } | |||||
| } | |||||
| int _install_yourself(const options& opts) { | |||||
| auto self_exe = current_executable(); | |||||
| auto dest_dir = opts.install_yourself.where == opts.install_yourself.user | |||||
| ? user_binaries_dir() | |||||
| : system_binaries_dir(); | |||||
| auto dest_path = dest_dir / "dds"; | |||||
| if constexpr (neo::os_is_windows) { | |||||
| dest_path += ".exe"; | |||||
| } | |||||
| if (fs::canonical(dest_path) == fs::canonical(self_exe)) { | |||||
| dds_log(error, "We cannot install over our own executable (.br.red[{}])"_styled, self_exe); | |||||
| return 1; | |||||
| } | |||||
| if (!fs::is_directory(dest_dir)) { | |||||
| if (opts.dry_run) { | |||||
| dds_log(info, "Would create directory .br.cyan[{}]"_styled, dest_dir); | |||||
| } else { | |||||
| dds_log(info, "Creating directory .br.cyan[{}]"_styled, dest_dir); | |||||
| fs::create_directories(dest_dir); | |||||
| } | |||||
| } | |||||
| if (opts.dry_run) { | |||||
| if (fs::is_symlink(dest_path)) { | |||||
| dds_log(info, "Would remove symlink .br.cyan[{}]"_styled, dest_path); | |||||
| } | |||||
| if (fs::exists(dest_path) && !fs::is_symlink(dest_path)) { | |||||
| if (opts.install_yourself.symlink) { | |||||
| dds_log( | |||||
| info, | |||||
| "Would overwrite .br.yellow[{0}] with a symlink .br.green[{0}] -> .br.cyan[{1}]"_styled, | |||||
| dest_path, | |||||
| self_exe); | |||||
| } else { | |||||
| dds_log(info, | |||||
| "Would overwrite .br.yellow[{}] with .br.cyan[{}]"_styled, | |||||
| dest_path, | |||||
| self_exe); | |||||
| } | |||||
| } else { | |||||
| if (opts.install_yourself.symlink) { | |||||
| dds_log(info, | |||||
| "Would create a symlink .br.green[{}] -> .br.cyan[{}]"_styled, | |||||
| dest_path, | |||||
| self_exe); | |||||
| } else { | |||||
| dds_log(info, | |||||
| "Would install .br.cyan[{}] to .br.yellow[{}]"_styled, | |||||
| self_exe, | |||||
| dest_path); | |||||
| } | |||||
| } | |||||
| } else { | |||||
| if (fs::is_symlink(dest_path)) { | |||||
| dds_log(info, "Removing old symlink file .br.cyan[{}]"_styled, dest_path); | |||||
| dds::remove_file(dest_path).value(); | |||||
| } | |||||
| if (opts.install_yourself.symlink) { | |||||
| if (fs::exists(dest_path)) { | |||||
| dds_log(info, "Removing previous file .br.cyan[{}]"_styled, dest_path); | |||||
| dds::remove_file(dest_path).value(); | |||||
| } | |||||
| dds_log(info, | |||||
| "Creating symbolic link .br.green[{}] -> .br.cyan[{}]"_styled, | |||||
| dest_path, | |||||
| self_exe); | |||||
| dds::create_symlink(self_exe, dest_path).value(); | |||||
| } else { | |||||
| dds_log(info, "Installing .br.cyan[{}] to .br.green[{}]"_styled, self_exe, dest_path); | |||||
| dds::copy_file(self_exe, dest_path, fs::copy_options::overwrite_existing).value(); | |||||
| } | |||||
| } | |||||
| if (opts.install_yourself.fixup_path_env) { | |||||
| fixup_path(opts); | |||||
| } | |||||
| if (!opts.dry_run) { | |||||
| dds_log(info, "Success!"); | |||||
| } | |||||
| return 0; | |||||
| } | |||||
| } // namespace | |||||
| int install_yourself(const options& opts) { | |||||
| return boost::leaf::try_catch( | |||||
| [&] { | |||||
| try { | |||||
| return _install_yourself(opts); | |||||
| } catch (...) { | |||||
| capture_exception(); | |||||
| } | |||||
| }, | |||||
| [](std::error_code ec, e_copy_file copy) { | |||||
| dds_log(error, | |||||
| "Failed to copy file .br.cyan[{}] to .br.yellow[{}]: .bold.red[{}]"_styled, | |||||
| copy.source, | |||||
| copy.dest, | |||||
| ec.message()); | |||||
| return 1; | |||||
| }, | |||||
| [](std::error_code ec, e_remove_file file) { | |||||
| dds_log(error, | |||||
| "Failed to delete file .br.yellow[{}]: .bold.red[{}]"_styled, | |||||
| file.value, | |||||
| ec.message()); | |||||
| return 1; | |||||
| }, | |||||
| [](std::error_code ec, e_symlink oper) { | |||||
| dds_log( | |||||
| error, | |||||
| "Failed to create symlink from .br.yellow[{}] to .br.cyan[{}]: .bold.red[{}]"_styled, | |||||
| oper.symlink, | |||||
| oper.target, | |||||
| ec.message()); | |||||
| return 1; | |||||
| }, | |||||
| [](e_system_error_exc e) { | |||||
| dds_log(error, "Failure while installing: {}", e.message); | |||||
| return 1; | |||||
| }); | |||||
| return 0; | |||||
| } | |||||
| } // namespace dds::cli::cmd |
| command build_deps; | command build_deps; | ||||
| command build; | command build; | ||||
| command compile_file; | command compile_file; | ||||
| command install_yourself; | |||||
| command pkg_create; | command pkg_create; | ||||
| command pkg_get; | command pkg_get; | ||||
| command pkg_import; | command pkg_import; | ||||
| return cmd::compile_file(opts); | return cmd::compile_file(opts); | ||||
| case subcommand::build_deps: | case subcommand::build_deps: | ||||
| return cmd::build_deps(opts); | return cmd::build_deps(opts); | ||||
| case subcommand::install_yourself: | |||||
| return cmd::install_yourself(opts); | |||||
| case subcommand::_none_:; | case subcommand::_none_:; | ||||
| } | } | ||||
| neo::unreachable(); | neo::unreachable(); |
| .name = "repoman", | .name = "repoman", | ||||
| .help = "Manage a dds package repository", | .help = "Manage a dds package repository", | ||||
| })); | })); | ||||
| setup_install_yourself_cmd(group.add_parser({ | |||||
| .name = "install-yourself", | |||||
| .help = "Have this dds executable install itself onto your PATH", | |||||
| })); | |||||
| } | } | ||||
| void setup_build_cmd(argument_parser& build_cmd) { | void setup_build_cmd(argument_parser& build_cmd) { | ||||
| void setup_pkg_search_cmd(argument_parser& pkg_repo_search_cmd) noexcept { | void setup_pkg_search_cmd(argument_parser& pkg_repo_search_cmd) noexcept { | ||||
| pkg_repo_search_cmd.add_argument({ | pkg_repo_search_cmd.add_argument({ | ||||
| .help = std::string( // | |||||
| "A name or glob-style pattern. Only matching packages will be returned. \n" | |||||
| "Searching is case-insensitive. Only the .italic[name] will be matched (not the \n" | |||||
| "version).\n\nIf this parameter is omitted, the search will return .italic[all] \n" | |||||
| "available packages."_styled), | |||||
| .help | |||||
| = "A name or glob-style pattern. Only matching packages will be returned. \n" | |||||
| "Searching is case-insensitive. Only the .italic[name] will be matched (not the \n" | |||||
| "version).\n\nIf this parameter is omitted, the search will return .italic[all] \n" | |||||
| "available packages."_styled, | |||||
| .valname = "<name-or-pattern>", | .valname = "<name-or-pattern>", | ||||
| .action = put_into(opts.pkg.search.pattern), | .action = put_into(opts.pkg.search.pattern), | ||||
| }); | }); | ||||
| .action = push_back_onto(opts.repoman.remove.pkgs), | .action = push_back_onto(opts.repoman.remove.pkgs), | ||||
| }); | }); | ||||
| } | } | ||||
| void setup_install_yourself_cmd(argument_parser& install_yourself_cmd) { | |||||
| install_yourself_cmd.add_argument({ | |||||
| .long_spellings = {"where"}, | |||||
| .help = "The scope of the installation. For .bold[system], installs in a global \n" | |||||
| "directory for all users of the system. For .bold[user], installs in a \n" | |||||
| "user-specific directory for executable binaries."_styled, | |||||
| .valname = "{user,system}", | |||||
| .action = put_into(opts.install_yourself.where), | |||||
| }); | |||||
| install_yourself_cmd.add_argument({ | |||||
| .long_spellings = {"dry-run"}, | |||||
| .help | |||||
| = "Do not actually perform any operations, but log what .italic[would] happen"_styled, | |||||
| .nargs = 0, | |||||
| .action = store_true(opts.dry_run), | |||||
| }); | |||||
| install_yourself_cmd.add_argument({ | |||||
| .long_spellings = {"no-modify-path"}, | |||||
| .help = "Do not attempt to modify the PATH environment variable", | |||||
| .nargs = 0, | |||||
| .action = store_false(opts.install_yourself.fixup_path_env), | |||||
| }); | |||||
| install_yourself_cmd.add_argument({ | |||||
| .long_spellings = {"symlink"}, | |||||
| .help = "Create a symlink at the installed location to the existing 'dds' executable\n" | |||||
| "instead of copying the executable file", | |||||
| .nargs = 0, | |||||
| .action = store_true(opts.install_yourself.symlink), | |||||
| }); | |||||
| } | |||||
| }; | }; | ||||
| } // namespace | } // namespace |
| build_deps, | build_deps, | ||||
| pkg, | pkg, | ||||
| repoman, | repoman, | ||||
| install_yourself, | |||||
| }; | }; | ||||
| /** | /** | ||||
| opt_path pkg_db_dir; | opt_path pkg_db_dir; | ||||
| // The `--log-level` argument | // The `--log-level` argument | ||||
| log::level log_level = log::level::info; | log::level log_level = log::level::info; | ||||
| // Any `--dry-run` argument | |||||
| bool dry_run = false; | |||||
| // The top-most selected subcommand | // The top-most selected subcommand | ||||
| enum subcommand subcommand; | enum subcommand subcommand; | ||||
| } remove; | } remove; | ||||
| } repoman; | } repoman; | ||||
| struct { | |||||
| enum where_e { | |||||
| system, | |||||
| user, | |||||
| } where | |||||
| = user; | |||||
| bool fixup_path_env = true; | |||||
| bool symlink = false; | |||||
| } install_yourself; | |||||
| /** | /** | ||||
| * @brief Attach arguments and subcommands to the given argument parser, binding those arguments | * @brief Attach arguments and subcommands to the given argument parser, binding those arguments | ||||
| * to the values in this object. | * to the values in this object. |
| #include "./fs.hpp" | #include "./fs.hpp" | ||||
| #include <dds/error/on_error.hpp> | |||||
| #include <dds/error/result.hpp> | |||||
| #include <fmt/core.h> | #include <fmt/core.h> | ||||
| #include <sstream> | #include <sstream> | ||||
| } | } | ||||
| fs::rename(tmp, dest); | fs::rename(tmp, dest); | ||||
| fs::remove_all(source); | fs::remove_all(source); | ||||
| } | |||||
| result<void> dds::copy_file(path_ref source, path_ref dest, fs::copy_options opts) noexcept { | |||||
| std::error_code ec; | |||||
| fs::copy_file(source, dest, opts, ec); | |||||
| if (ec) { | |||||
| return new_error(DDS_E_ARG(e_copy_file{source, dest}), ec); | |||||
| } | |||||
| return {}; | |||||
| } | |||||
| result<void> dds::remove_file(path_ref file) noexcept { | |||||
| std::error_code ec; | |||||
| fs::remove(file, ec); | |||||
| if (ec) { | |||||
| return new_error(DDS_E_ARG(e_remove_file{file}), ec); | |||||
| } | |||||
| return {}; | |||||
| } | |||||
| result<void> dds::create_symlink(path_ref target, path_ref symlink) noexcept { | |||||
| std::error_code ec; | |||||
| if (fs::is_directory(target)) { | |||||
| fs::create_directory_symlink(target, symlink, ec); | |||||
| } else { | |||||
| fs::create_symlink(target, symlink, ec); | |||||
| } | |||||
| if (ec) { | |||||
| return new_error(DDS_E_ARG(e_symlink{symlink, target}), ec); | |||||
| } | |||||
| return {}; | |||||
| } | |||||
| result<void> dds::write_file(path_ref dest, std::string_view content) noexcept { | |||||
| std::error_code ec; | |||||
| auto outfile = dds::open(dest, std::ios::binary | std::ios::out, ec); | |||||
| if (ec) { | |||||
| return new_error(DDS_E_ARG(e_write_file_path{dest}), ec); | |||||
| } | |||||
| errno = 0; | |||||
| outfile.write(content.data(), content.size()); | |||||
| auto e = errno; | |||||
| if (!outfile) { | |||||
| return new_error(std::error_code(e, std::system_category()), | |||||
| DDS_E_ARG(e_write_file_path{dest})); | |||||
| } | |||||
| return {}; | |||||
| } | } |
| #pragma once | #pragma once | ||||
| #include <dds/error/result_fwd.hpp> | |||||
| #include <filesystem> | #include <filesystem> | ||||
| #include <fstream> | #include <fstream> | ||||
| #include <string> | #include <string> | ||||
| std::fstream open(const fs::path& filepath, std::ios::openmode mode, std::error_code& ec); | std::fstream open(const fs::path& filepath, std::ios::openmode mode, std::error_code& ec); | ||||
| std::string slurp_file(const fs::path& path, std::error_code& ec); | std::string slurp_file(const fs::path& path, std::error_code& ec); | ||||
| struct e_write_file_path { | |||||
| fs::path value; | |||||
| }; | |||||
| [[nodiscard]] result<void> write_file(const fs::path& path, std::string_view content) noexcept; | |||||
| inline std::fstream open(const fs::path& filepath, std::ios::openmode mode) { | inline std::fstream open(const fs::path& filepath, std::ios::openmode mode) { | ||||
| std::error_code ec; | std::error_code ec; | ||||
| auto ret = dds::open(filepath, mode, ec); | auto ret = dds::open(filepath, mode, ec); | ||||
| void safe_rename(path_ref source, path_ref dest); | void safe_rename(path_ref source, path_ref dest); | ||||
| struct e_copy_file { | |||||
| fs::path source; | |||||
| fs::path dest; | |||||
| }; | |||||
| struct e_remove_file { | |||||
| fs::path value; | |||||
| }; | |||||
| struct e_symlink { | |||||
| fs::path symlink; | |||||
| fs::path target; | |||||
| }; | |||||
| [[nodiscard]] result<void> | |||||
| copy_file(path_ref source, path_ref dest, fs::copy_options opts = {}) noexcept; | |||||
| [[nodiscard]] result<void> remove_file(path_ref file) noexcept; | |||||
| [[nodiscard]] result<void> create_symlink(path_ref target, path_ref symlink) noexcept; | |||||
| } // namespace file_utils | } // namespace file_utils | ||||
| } // namespace dds | } // namespace dds |
| return text_styler{str, should}.render(); | return text_styler{str, should}.render(); | ||||
| } | } | ||||
| std::string_view detail::cached_rendering(const char* ptr) noexcept { | |||||
| const std::string& detail::cached_rendering(const char* ptr) noexcept { | |||||
| thread_local std::map<const char*, std::string> cache; | thread_local std::map<const char*, std::string> cache; | ||||
| auto found = cache.find(ptr); | auto found = cache.find(ptr); | ||||
| if (found == cache.end()) { | if (found == cache.end()) { |
| std::string stylize(std::string_view text, should_style = should_style::detect); | std::string stylize(std::string_view text, should_style = should_style::detect); | ||||
| namespace detail { | namespace detail { | ||||
| std::string_view cached_rendering(const char* ptr) noexcept; | |||||
| const std::string& cached_rendering(const char* ptr) noexcept; | |||||
| } | } | ||||
| inline namespace literals { | inline namespace literals { | ||||
| inline namespace styled_literals { | inline namespace styled_literals { | ||||
| inline std::string_view operator""_styled(const char* str, std::size_t) { | |||||
| inline const std::string& operator""_styled(const char* str, std::size_t) { | |||||
| return detail::cached_rendering(str); | return detail::cached_rendering(str); | ||||
| } | } | ||||