Browse Source

'install-yourself' subcommand (Linux support)

default_compile_flags
vector-of-bool 3 years ago
parent
commit
d30d88dde4
8 changed files with 458 additions and 8 deletions
  1. +322
    -0
      src/dds/cli/cmd/install_yourself.cpp
  2. +3
    -0
      src/dds/cli/dispatch_main.cpp
  3. +40
    -5
      src/dds/cli/options.cpp
  4. +13
    -0
      src/dds/cli/options.hpp
  5. +50
    -0
      src/dds/util/fs.cpp
  6. +27
    -0
      src/dds/util/fs.hpp
  7. +1
    -1
      src/fansi/styled.cpp
  8. +2
    -2
      src/fansi/styled.hpp

+ 322
- 0
src/dds/cli/cmd/install_yourself.cpp View File

#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

+ 3
- 0
src/dds/cli/dispatch_main.cpp View File

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();

+ 40
- 5
src/dds/cli/options.cpp View File

.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

+ 13
- 0
src/dds/cli/options.hpp View File

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.

+ 50
- 0
src/dds/util/fs.cpp View File

#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 {};
} }

+ 27
- 0
src/dds/util/fs.hpp View File

#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

+ 1
- 1
src/fansi/styled.cpp View File

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()) {

+ 2
- 2
src/fansi/styled.hpp View File

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);
} }



Loading…
Cancel
Save