123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 | // `tomo build`: compile a Tomo program to a standalone executable
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "../config.h"
#include "../environment.h"
#include "../packages.h"
#include "../stdlib/bool.h"
#include "../stdlib/list.h"
#include "../stdlib/path.h"
#include "../stdlib/print.h"
#include "commands.h"
#include "common.h"
#include "compilation.h"
static OptionalPath_t file = NULL, output = NULL, prefix = NULL, package_sources = NULL;
static OptionalBool_t do_install = false, skip_confirm = false, skip_source = false;
static cli_arg_t build_spec[] = {
{"file", &file, &Path$info, .positional = true, .required = true, .metavar = "file.tm",
.description = "the program to compile"}, //
{"output", &output, &Path$info, .short_flag = 'o',
.description = "where to put the executable (defaults to a sibling of the .tm file)"}, //
{"package-sources", &package_sources, &Path$info, .metavar = "dir",
.description = "a directory holding package sources by digest, used instead of downloading pinned packages"}, //
{"skip-source", &skip_source, &Bool$info,
.description = "don't embed the program's sources in the executable (`tomo source` can't unpack it)"}, //
{"install", &do_install, &Bool$info,
.description = "also install the executable (and its manpage) into a prefix's bin/ and man/"}, //
{"prefix", &prefix, &Path$info, .metavar = "dir",
.description = "the install prefix for --install (defaults to this Tomo installation's prefix)"}, //
{"yes", &skip_confirm, &Bool$info, .short_flag = 'y',
.description = "when installing, overwrite existing files without asking"}, //
OPTIMIZATION_FLAG, //
INSTRUMENT_FLAG, //
DEBUG_FLAG, //
VERBOSE_FLAG, //
QUIET_FLAG, //
};
// The install prefix: --prefix if given (relative to the working directory),
// otherwise this Tomo installation's own prefix.
static Path_t install_prefix(void) {
return prefix != NONE_PATH ? Path$expand_home(Path$resolved(prefix, Path$current_dir())) : Path$from_str(TOMO_PATH);
}
// Whether `dir` (resolved) is one of the directories on $PATH.
static bool dir_on_path(Path_t dir) {
char real_dir[PATH_MAX];
if (!realpath(Path$as_c_string(dir), real_dir)) return false;
const char *path = getenv("PATH");
if (!path) return false;
for (const char *p = path; *p;) {
size_t len = strcspn(p, ":");
if (len > 0 && len < PATH_MAX) {
char entry[PATH_MAX], real_entry[PATH_MAX];
memcpy(entry, p, len);
entry[len] = '\0';
if (realpath(entry, real_entry) && strcmp(real_entry, real_dir) == 0) return true;
}
p += len + (p[len] == ':' ? 1 : 0);
}
return false;
}
// Copy the freshly built executable (and its generated manpage, if any) into
// the prefix's bin/ and man/man1/. `tomo uninstall <name>` reverses this.
// Prompts before overwriting existing files unless --yes was given.
static void install_program(Path_t src_file, Path_t exe_path) {
Path_t prefix_dir = install_prefix();
OptionalText_t name = Path$base_name(exe_path);
if (name.tag == TEXT_NONE) print_err("This executable's name is not valid UTF-8: ", exe_path);
Path_t bin_dir = Path$child(prefix_dir, Text("bin"));
Path_t man_dir = Path$child(Path$child(prefix_dir, Text("man")), Text("man1"));
Path_t bin_dest = Path$child(bin_dir, name);
Path_t man_dest = Path$child(man_dir, Texts(name, ".1"));
Path_t manpage_file = build_file(Path$with_extension(src_file, Text(".1"), true), "");
bool have_manpage = Path$is_file(manpage_file, true);
// Confirm before clobbering anything already installed under those names:
bool bin_exists = Path$exists(bin_dest), man_exists = have_manpage && Path$exists(man_dest);
if ((bin_exists || man_exists) && skip_confirm != true) {
if (bin_exists) fprint(stderr, "\x1b[33mWarning:\x1b[m \x1b[1m", bin_dest, "\x1b[m already exists.");
if (man_exists) fprint(stderr, "\x1b[33mWarning:\x1b[m \x1b[1m", man_dest, "\x1b[m already exists.");
if (!isatty(STDIN_FILENO)) print_err("Refusing to overwrite; re-run with --yes to overwrite existing files.");
fprint_inline(stderr, "Overwrite? [y/N] ");
fflush(stderr);
char answer[16] = {};
if (!fgets(answer, sizeof(answer), stdin) || (answer[0] != 'y' && answer[0] != 'Y'))
print_err("Not installing ", name);
}
xsystem("mkdir -p '", bin_dir, "' '", man_dir, "'");
xsystem("cp -v '", exe_path, "' '", bin_dest, "'");
if (have_manpage) xsystem("cp -v '", manpage_file, "' '", man_dest, "'");
print("Installed \033[1m", name, "\033[m into ", prefix_dir);
if (!dir_on_path(bin_dir))
fprint(stderr, "\x1b[33mWarning:\x1b[m ", bin_dir, " is not on your $PATH, so `", name,
"` won't be found by name until you add it.");
}
static int cmd_build(cli_command_t *self, List_t extra_args) {
(void)self, (void)extra_args;
set_default_logs(LOG_BUILD);
if (package_sources != NONE_PATH) set_package_sources_dir(package_sources);
if (prefix != NONE_PATH && do_install != true) print_err("`--prefix` only applies together with `--install`");
// `--install` puts a native binary into the prefix, so it can't be combined
// with cross-compilation (the binary wouldn't run here) and needs write
// access to the prefix, so check both before doing the build work:
if (do_install) {
if (cross_compiling) print_err("`--install` can't be combined with --target: the binary wouldn't run here");
require_writable_prefix(Path$as_c_string(install_prefix()));
}
// A built executable is a persistent artifact, so default to the highest
// safe optimization level and the size-reducing link flags; -O overrides
// the level:
configure_codegen(opt_flag.tag == TEXT_NONE ? Text("3") : opt_flag, /*optimize=*/true);
List_t files = normalize_tm_paths(List(file));
Path_t path = *(Path_t *)files.data;
Path_t exe_path;
if (output != NULL) {
exe_path = Path$resolved(output, Path$current_dir());
} else {
exe_path = get_exe_path(path);
// Put the executable as a sibling to the .tm file instead of in the
// .tomo directory. Cross-compiled executables get the target platform
// as a suffix (foo.aarch64-macos) so they don't collide with the
// native executable or each other:
OptionalText_t exe_name = Path$base_name(exe_path);
if (exe_name.tag == TEXT_NONE) print_err("This executable's name is not valid UTF-8: ", exe_path);
if (cross_compiling) exe_name = Texts(exe_name, ".", target);
OptionalPath_t sibling = Path$sibling(path, exe_name);
assert(sibling); // A source file always has a directory
exe_path = sibling;
}
env_t *env = global_env(source_mapping, instrument, debugging);
List_t object_files = EMPTY_LIST, extra_ldlibs = EMPTY_LIST;
compile_files(env, List(path), &object_files, &extra_ldlibs, COMPILE_EXE);
compile_executable(env, path, exe_path, object_files, extra_ldlibs, /*embed_git_info=*/true,
/*embed_source=*/skip_source != true);
if (do_install) install_program(path, exe_path);
return 0;
}
cli_command_t build_command = {
.name = "build",
.summary = "Compile a Tomo program to a standalone executable",
.description = "Compiles the .tm program to a standalone executable next to the source\n"
"file (or at -o). With --install, the executable and its manpage are also\n"
"copied into a prefix's bin/ and man/man1/ (the prefix defaults to this Tomo\n"
"installation's, or --prefix a different one); existing files are overwritten\n"
"only after confirmation, or with --yes. Remove installed programs with\n"
"`tomo uninstall <name>`.",
.spec_len = sizeof(build_spec) / sizeof(build_spec[0]),
.spec = build_spec,
.handler = cmd_build,
};
|